'use strict';
/**
* This is the Project Management API for Zanata
* @exports zanata/project
*
* @requires underscore
* @requires co
* @requires util
* @requires fs
* @requires path
* @requires events
* @requires glob
* @requires xdg
* @requires ssl-root-cas
* @requires zanata/api/projectsResource
* @requires zanata/api/projectResource
* @requires zanata/api/projectLocalesResource
* @requires zanata/api/projectIterationResource
* @requires zanata/api/projectIterationLocalesResource
* @requires zanata/api/sourceDocResource
* @requires zanata/api/translatedDocResource
* @requires zanata/api/statisticsResource
* @requires zanata/fileMapping
* @requires zanata/config
* @requires zanata/gettext
* @requires zanata/etag
* @requires zanata/etagCache
*/
const _ = require('underscore');
const co = require('co');
const util = require('util');
const fs = require('fs');
const path = require('path');
const EventEmitter = require('events').EventEmitter;
const Glob = require('glob').Glob;
const xdg = require('xdg').basedir;
const ssl = require('ssl-root-cas').inject();
const debuglog = util.debuglog('zanata');
// read certificates before loading APIs
// otherwise it won't be applied.
require('glob').sync(path.join(xdg.configHome(), 'zanata-js', 'certs', '*.@(pem|crt)'),
{nodir: true, dot: false})
.forEach(function(v) {
debuglog('Loading ' + v);
ssl.addFile(v);
});
const psr = require('./api/projectsResource.js').ProjectsResource;
const pr = require('./api/projectResource.js').ProjectResource;
const plr = require('./api/projectLocalesResource.js').ProjectLocalesResource;
const pir = require('./api/projectIterationResource.js').ProjectIterationResource;
const pilr = require('./api/projectIterationLocalesResource.js').ProjectIterationLocalesResource;
const sdr = require('./api/sourceDocResource.js').SourceDocResource;
const tdr = require('./api/translatedDocResource.js').TranslatedDocResource;
const sr = require('./api/statisticsResource.js').StatisticsResource;
const fmr = require('./fileMapping.js').FileMappingRule;
const em = require('../util/errmsg.js');
const yorn = require('../util/y-or-n.js');
const Config = require('./config.js').Config;
const Gettext = require('./gettext.js').Gettext;
const ETag = require('./etag.js');
const ETagCache = require('./etagCache.js');
const errmsg = em.errmsg;
const warnmsg = em.warnmsg;
/**
* @class
* @classdesc The base class to deal with the operations on the client side to the Zanata.
* @extends EventEmitter
*/
class Project extends EventEmitter {
/**
* All of responses from the server will be sent through the events.
*
* @constructor
* @see {@link module:zanata/config~Config}
* @param {Object} param - The options for Project. most of the properties will be stored into {@link module:zanata/config~Config}.
* @param {string} param.url - The URL to the Zanata server where you want to connect to.
* @param {string} [param.username] - The username you want to connect to the Zanata with.
* @param {string} [param.api-key] - The API key may be required to authorize for certain access on the Zanata.
*/
constructor(params) {
super();
let configFile = params && params['project-config'];
let self = this;
self.params = params;
self.config = new Config(configFile);
if (params instanceof Object) {
Object.keys(params).forEach(function(k) {
if (params[k] != undefined)
self.config.set(k, params[k]);
});
}
}
/**
* Obtain the projects and the information available on the Zanata
*
* @param {string[]} [filter=['id', 'name', 'status']] - The properties you want to obtain from the project.
*
* @fires module:zanata/project~Project#fail
* @fires module:zanata/project~Project#data_list
*/
list(filter) {
let p = new psr(this.config.get('config.url'));
p
.setAuthUser(this.config.get('username'))
.setAuthToken(this.config.get('api-key'))
.get()
.then((d) => {
let resp = d.response;
let data = d.json;
if (resp.statusCode !== 200)
throw new Error(errmsg(p.httpStatusMessage(resp, data)));
let delobj = Object.keys(data[0]).filter((v) => (filter.indexOf(v) < 0));
data.forEach((o) => {
delobj.forEach((v) => delete o[v]);
});
/**
* data event from list method
*
* @event module:zanata/project~Project#data_list
* @type {object[]}
* @property {string} id - the project id
* @property {string} defaultType - the default project type
* @property {string} name - the project name
* @property {string} status - the project status
* @property {string} description - the project description
* @property {string} sourceViewURL - the source view URL for the project
* @property {string} sourceCheckoutURL - the source URL to check out
*
* @see {@link https://zanata.ci.cloudbees.com/job/zanata-api-site/site/zanata-common-api/rest-api-docs/json_Project.html|Project data type}
*/
this.emit('data_list', data);
})
/**
* fail event
*
* @event module:zanata/project~Project#fail
* @type {Error}
*/
.catch((e) => this.emit('fail', e));
return this;
}
/**
* Create a project
*
* @param {object} [params] - the parameters to create a project. most properties will be stored into {@link module:zanata/config~Config}.
* @param {string} params.project - the project id
* @param {string} params.project-name - the project name
* @param {string} params.project-type - the default project type. it must be one of 'File', 'Gettext', 'Podir', 'Properties', 'Utf8Properties', 'Xliff', or 'Xml'
* @param {string} [params.description] - the project description
*
* @see {@link module:zanata/config~Config}
*
* @fires module:zanata/project~Project#fail
* @fires module:zanata/project~Project#data_create
*/
create(params) {
let projectTypes = ['File', 'Gettext', 'Podir', 'Properties', 'Utf8Properties', 'Xliff', 'Xml'];
let self = this;
let config = util._extend(new Config(), self.config);
Object.keys(params).forEach((k) => {
if (params[k] != undefined)
config.set(k, params[k]);
});
if (config.get('project') == undefined) {
self.emit('fail', new Error('No project id to create'));
} else if (config.get('project-name') == undefined) {
self.emit('fail', new Error('No project name to create'));
} else if (config.get('project-type') == undefined) {
self.emit('fail', new Error('No default project type to create'));
} else if (projectTypes.map(v => v.toLowerCase()).indexOf(config.get('project-type').toLowerCase()) < 0) {
self.emit('fail', new Error(util.format('Invalid default project type. must be one of %s but: %s',
projectTypes.join(', '),
config.get('project-type'))));
} else {
let p = new pr(self.config.get('config.url'));
p
.setAuthUser(self.config.get('username'))
.setAuthToken(self.config.get('api-key'))
.put({id: config.get('project'),
defaultType: config.get('project-type'),
name: config.get('project-name'),
description: config.get('description')})
.then((d) => {
let resp = d.response;
let data = d.json || d.data;
if (resp.statusCode === 200) {
return 'Successfully updated';
} else if (resp.statusCode === 201) {
return 'Successfully created';
} else if (resp.statusCode === 401) {
throw new Error('Unauthorized operation');
// } else if (resp.statusCode === 403) {
} else {
throw new Error(errmsg(p.httpStatusMessage(resp, data)));
}
})
/**
* data event from create method. this contains the result string.
*
* @event module:zanata/project~Project#data_create
* @type {string}
*/
.then(v => this.emit('data_create', v))
.catch(e => self.emit('fail', e));
}
return self;
}
/**
* Obtain the project information from the Zanata.
*
* @param {string} id - the project id to obtain the information
* @param {boolean} containLocales - true to contain the locale information in the result, otherwise will be suppressed.
*
* @fires module:zanata/project~Project#fail
* @fires module:zanata/project~Project#data_info
*/
info(id, containLocales) {
let self = this;
let p = new pr(self.config.get('config.url'));
p
.setAuthUser(self.config.get('username'))
.setAuthToken(self.config.get('api-key'))
.get(id)
.then((d) => {
let resp = d.response;
let data = d.json || d.data;
let fLocales;
if (resp.statusCode === 404) {
throw new Error('No such projects on the server: ' + id);
} else if (resp.statusCode !== 200) {
throw new Error(errmsg(p.httpStatusMessage(resp, data)));
}
if (containLocales) {
fLocales = () => {
return new plr(self.config.get('config.url'))
.get(id)
.then((dd) => {
let resp = dd.response;
let data = dd.json || dd.data;
if (resp.statusCode === 404) {
throw new Error('No such projects on the server: ' + id);
} else if (resp.statusCode !== 200) {
throw new Error(errmsg(dd.self.httpStatusMessage(resp, dd.data)));
}
return data;
});
};
} else {
fLocales = () => Promise.resolve(null);
}
if (data.iterations) {
let cb = data.iterations.filter((o)=>o.status === 'ACTIVE').map((o) => {
let pi = new pir(self.config.get('config.url'));
// XXX: should check href and type?
return pi
.setAuthUser(self.config.get('username'))
.setAuthToken(self.config.get('api-key'))
.get(id, o.id)
.then((d) => {
let resp = d.response;
let data = d.json || d.data;
if (resp.statusCode === 404) {
// ignore this error
return null;
} else if (resp.statusCode === 200) {
return data;
} else {
throw new Error(errmsg(pi.httpStatusMessage(resp, data)));
}
});
});
Promise.all(cb)
.then((v) => {
delete data.iterations;
data.versions = v.filter((x) => (x != undefined));
return fLocales();
})
.then((v) => {
data.locales = v;
self.emit('data_info', data);
})
.catch((e) => self.emit('fail', e));
} else {
/**
* data event from info method.
*
* @event module:zanata/project~Project#data_info
* @type {object}
* @property {string} id - the project id
* @property {string} defaultType - the default project type
* @property {string} name - the project name
* @property {string} status - the project status
* @property {string} description - the project description
* @property {string} sourceViewURL - the source view URL for the project
* @property {string} sourceCheckoutURL - the source URL to check out
* @property {object[]} versions - the version-related information
* @property {string} versions.id - the version id
* @property {string} versions.defaultType - the project version type
* @property {string} versions.status - the status
* @property {object[]} [locales] - the locale name available in this project
* @property {object} locales.localeId - the LocaleId object
* @property {string} locales.localeId.id - BCP-47 language tag
* @property {string} [locales.displayName] - the display name of the locale
* @property {string} [locales.alias] - the alias name
* @property {string} [locales.nativeName]
* @property {boolean} locales.enabled
* @property {boolean} locales.enabledByDefault
* @property {string} [locales.pluralForms]
*
* @see {@link https://zanata.ci.cloudbees.com/job/zanata-api-site/site/zanata-common-api/rest-api-docs/json_Project.html|Project data type}
* @see {@link https://zanata.ci.cloudbees.com/job/zanata-api-site/site/zanata-common-api/rest-api-docs/json_LocaleId.html|LocaleId data type}
* @see {@link https://zanata.ci.cloudbees.com/job/zanata-api-site/site/zanata-common-api/rest-api-docs/json_LocaleDetails.html|LocaleDetails data type}
*/
fLocales()
.then((v) => {
data.locales = v;
self.emit('data_info', data);
})
.catch((e) => self.emit('fail', e));
}
});
return self;
}
/**
* Create a version in the project.
*
* @param {string} projectId - the project id
* @param {string} versionId - the version id
* @param {object} params - the parameters to create a version. most properties will be stored into {@link module:zanata/config~Config}.
* @param {string} params.project-type - the default project type. it must be one of 'File', 'Gettext', 'Podir', 'Properties', 'Utf8Properties', 'Xliff', or 'Xml'
*
* @see {@link module:zanata/config~Config}
*
* @fires module:zanata/project~Project#fail
* @fires module:zanata/project~Project#data_create
*/
createVersion(projectId, versionId, params) {
let self = this;
let pi = new pir(self.config.get('config.url'));
let config = util._extend(new Config(), self.config);
if (params == undefined)
params = {};
Object.keys(params).forEach((k) => {
if (params[k] != undefined)
config.set(k, params[k]);
});
if (projectId == undefined)
projectId = config.get('project');
pi
.setAuthUser(self.config.get('username'))
.setAuthToken(self.config.get('api-key'))
.get(projectId, versionId)
.then((d) => {
let resp = d.response;
let data = d.json || d.data;
if (resp.statusCode === 200)
throw new Error(util.format("Version '%s' already exists", versionId));
else if (resp.statusCode !== 404)
throw new Error(errmsg(pi.httpStatusMessage(resp, data)));
return pi
.put(projectId,
versionId,
{id: versionId, status: 'ACTIVE', projectType: self.config.get('project-type')})
.then((d) => {
let resp = d.response;
let data = d.json || d.data;
if (resp.statusCode === 200)
throw new Error(util.format("Version '%s' already exists", versionId));
else if (resp.statusCode === 201)
self.emit('data_create', 'Successfully created');
else if (resp.statusCode === 404)
throw new Error(util.format("Project '%s' does not exist", projectId));
else if (resp.statusCode === 401)
throw new Error('Unauthorized operation');
else
throw new Error(errmsg(pi.httpStatusMessage(resp, data)));
});
})
.catch((e) => self.emit('fail', e));
return self;
}
/**
* Obtain the information about the version for the project
*
* @param {string} projectId - the project id
* @param {string} versionId - the version id
* @param {boolean} containLocales - true to contain the locale information in the result, otherwise will be suppressed.
*
* @fires module:zanata/project~Project#fail
* @fires module:zanata/project~Project#data_version_info
*/
versionInfo(projectId, versionId, containLocales) {
let self = this;
let p = new pr(self.config.get('config.url'));
if (projectId == undefined)
projectId = self.config.get('project');
p
.setAuthUser(self.config.get('username'))
.setAuthToken(self.config.get('api-key'))
.get(projectId)
.then((d) => {
let resp = d.response;
if (resp.statusCode < 200 || resp.statusCode >= 300)
throw new Error(errmsg(p.httpStatusMessage(resp, d.data)));
let data = d.json;
if (!data.iterations) {
// if no locales available, return an empty array.
return {data: []};
// self.emit('data', []);
}
let avail = data.iterations.map((o) => o.id);
let ret = data.iterations.filter((o) => (o.id === versionId));
if (ret.length === 0)
throw new Error(util.format('No such version available in %s: %s\nAvailable versions are: %s',
projectId, versionId, avail.join(', ')));
// duplicate version shouldn't be available
ret = ret[0];
return {version: ret.id};
})
.then((d) => {
if (d.data)
return d.data;
let pi = new pir(self.config.get('config.url'));
// XXX: should check href and type?
return pi
.setAuthUser(self.config.get('username'))
.setAuthToken(self.config.get('api-key'))
.get(projectId, d.version)
.then((dd) => {
let resp = dd.response;
let json = dd.json;
if (resp.statusCode < 200 || resp.statusCode >= 300)
throw new Error(errmsg(pi.httpStatusMessage(resp, dd.data)));
if (containLocales) {
let pil = new pilr(self.config.get('config.url'));
return pil
.setAuthUser(self.config.get('username'))
.setAuthToken(self.config.get('api-key'))
.get(projectId, d.version)
.then((ddd) => {
let resp = ddd.response;
if (resp.statusCode >= 200 && resp.statusCode < 300) {
json.locales = ddd.json;
return json;
} else {
throw new Error(errmsg(pil.httpStatusMessage(resp, ddd.data)));
}
});
} else {
return json;
}
});
})
/**
* data event from versionInfo method.
*
* @event module:zanata/project~Project#data_version_info
* @type {object}
* @property {string} id - the version id
* @property {string} status - the status
* @property {string} projectType - the project type
* @property {object[]} [locales] - the locale name available in this project
* @property {object} locales.localeId - the LocaleId object
* @property {string} locales.localeId.id - BCP-47 language tag
* @property {string} [locales.displayName] - the display name of the locale
* @property {string} [locales.alias] - the alias name
* @property {string} [locales.nativeName]
* @property {boolean} locales.enabled
* @property {boolean} locales.enabledByDefault
* @property {string} [locales.pluralForms]
*
* @see {@link https://zanata.ci.cloudbees.com/job/zanata-api-site/site/zanata-common-api/rest-api-docs/json_ProjectIteration.html|ProjectIteration data type}
* @see {@link https://zanata.ci.cloudbees.com/job/zanata-api-site/site/zanata-common-api/rest-api-docs/json_LocaleId.html|LocaleId data type}
* @see {@link https://zanata.ci.cloudbees.com/job/zanata-api-site/site/zanata-common-api/rest-api-docs/json_LocaleDetails.html|LocaleDetails data type}
*/
.then((v) => self.emit('data_version_info', v))
.catch((e) => self.emit('fail', e));
return self;
}
__pullSourceCb(projectId, versionId, docId, etag, params, callback) {
let self = this;
let sd = new sdr(self.config.get('config.url'));
if (projectId == undefined)
projectId = self.config.get('project');
if (versionId == undefined)
versionId = self.config.get('project-version');
let verbose = params && params.verbose || 0;
let force = params && params.force || false;
let label;
let payload;
if (!force) {
etag.find(docId, versionId, 'en', (cache) => {
if (cache) {
sd.setETag(cache.getServerETag());
payload = cache.getPayload();
}
});
}
if (verbose > 2)
label = util.format('Downloading %s', docId);
if (label)
console.time(label);
sd
.setAuthUser(self.config.get('username'))
.setAuthToken(self.config.get('api-key'))
.getDoc(projectId, versionId, docId)
.then((d) => {
let resp = d.response;
let data = d.json || d.data;
let updateETag = false;
if (resp.statusCode === 304)
self.emit('warning', 'Source for ' + docId + " wasn't modified. using the cache");
else if (resp.statusCode !== 200)
throw new Error(errmsg(sd.httpStatusMessage(resp, data)));
else
updateETag = true;
if (label)
console.timeEnd(label);
if (updateETag) {
etag
.add(new ETagCache()
.setDocId(docId)
.setVersionId(versionId)
.setLanguage('en')
.setPayload(data)
.setServerETag(sd.getETag()))
.save();
payload = data;
}
callback(null, payload);
})
.catch((e) => callback(e));
return self;
}
_pullSourceCb(projectId, versionId, docId, etag, params, callback) {
this.__pullSourceCb(projectId, versionId, docId, etag, params, (e, d) => {
if (e) {
callback(e);
} else {
Gettext.json2po(d)
.then((data) => callback(null, {name: docId, type: 'pot', data: data}))
.catch((e) => callback(e));
}
});
return this;
}
/**
* Pull the source document from the Zanata
*
* @param {string} projectId - the project id
* @param {string} versionId - the version id
* @param {string} docId - the document id
* @param {object} [params] - the parameters to pull the source document
* @param {string} [params.potdir='./po'] - the place where store the cache file
* @param {number} [params.verbose=false] - Show more progress messages verbosely.
* @param {boolean} [params.force=false] - Transfer the document without the cache
*
* @fires module:zanata/project~Project#fail
* @fires module:zanata/project~Project#warning
* @fires module:zanata/project~Project#data_pull
* @fires module:zanata/project~Project#end_pull
*/
pullSource(projectId, versionId, docId, params) {
let self = this;
let potdir = params && params.potdir || './po';
let etag = new ETag(path.join(potdir, 'etag-cache.json'));
self._pullSourceCb(projectId, versionId, docId, etag, params, (e, d) => {
if (e) {
self.emit('fail', e);
} else {
self.emit('data_pull', d);
self.emit('end_pull', [{name: docId, type: 'pot', pulled: true}]); // for compatibility to other APIs
}
});
return self;
}
/**
* warning event from pull/push methods. this contains the warning messages where happened during processing
* and not that important more than stopping.
*
* @event module:zanata/project~Project#warning
* @type {string}
*/
/**
* data event from pull methods. this is emitted when any data is received from the Zanata on pulling.
*
* @event module:zanata/project~Project#data_pull
* @type {object}
* @property {string} name - the document id
* @property {string} type - the document type. 'pot' for the source document and 'po' for the translation
* @property {string} data - the contents for POT file
* @property {string} [locale] - the locale name for document. this is only available when pulling translations
*/
/**
* end event for pull methods. this is emitted when all of pulling is finished.
*
* @event module:zanata/project~Project#end_pull
* @type {object[]}
* @property {string} name - the document id
* @property {string} type - the document type. 'pot' for the source document and 'po' for the translation
* @property {boolean} pulled - whether the document was actually pulled from the Zanata or the cache due to no changes since the last pull.
* @property {string} [locale] - the locale name for document. this is only available when pulling translations
*/
/**
* Pull the source documents from the Zanata
*
* @param {string} projectId - the project id
* @param {string} versionId - the project version id
* @param {object} params - the parameters to pull the source documents from the Zanata
* @param {string} [params.potdir='./po'] - the place where store the cache file
* @param {number} [params.verbose=false] - Show more progress messages verbosely.
* @param {boolean} [params.force=false] - Transfer the document without the cache
*
* @fires module:zanata/project~Project#fail
* @fires module:zanata/project~Project#warning
* @fires module:zanata/project~Project#data_pull
* @fires module:zanata/project~Project#end_pull
*/
pullSources(projectId, versionId, params) {
let self = this;
let potdir = params && params.potdir || './po';
let etag = new ETag(path.join(potdir, 'etag-cache.json'));
if (projectId == undefined)
projectId = self.config.get('project');
if (versionId == undefined)
versionId = self.config.get('project-version');
let sd = new sdr(self.config.get('config.url'))
.setAuthUser(self.config.get('username'))
.setAuthToken(self.config.get('api-key'))
.get(projectId, versionId)
.then((d) => {
let resp = d.response;
let data = d.json || d.data;
if (resp.statusCode === 404)
throw new Error('No such resources available on the server: [project: ' + projectId + ', version: ' + versionId + ']');
else if (resp.statusCode !== 200)
throw new Error(errmsg(sd.httpStatusMessage(resp, data)));
// contains document data in Array
let promises = data.map((o) => {
return new Promise((resolve, reject) => {
self._pullSourceCb(projectId, versionId, o.name, etag, params, (e, d) => {
if (e)
reject(e);
else
self.emit('data_pull', d);
resolve(o);
});
});
});
return Promise.all(promises)
.then((d) => d.map((v) => {
return {name: v.name, type: 'pot', pulled: true};
}))
.catch((e) => self.emit('fail', e));
})
.then((d) => self.emit('end_pull', d))
.catch((e) => self.emit('fail', e));
return self;
}
_pullTranslationCb(projectId, versionId, docId, potObj, locale, etag, params, callback) {
let self = this;
if (projectId == undefined)
projectId = self.config.get('project');
if (versionId == undefined)
versionId = self.config.get('project-version');
let verbose = params && params.verbose || 0;
let skeletons = params && params.skeletons || false;
let force = params && params.force || false;
let label;
let payload;
let td = new tdr(self.config.get('config.url'));
if (!force) {
etag.find(docId, versionId, locale, (cache) => {
if (cache) {
td.setETag(cache.getServerETag());
payload = cache.getPayload();
}
});
}
if (verbose > 2)
label = util.format('Downloading %s translation for %s', locale, docId);
if (label)
console.time(label);
td
.setAuthUser(self.config.get('username'))
.setAuthToken(self.config.get('api-key'))
.get(projectId, versionId, docId, locale, skeletons)
.then((d) => {
let resp = d.response;
let data = d.json || d.data;
if (resp.statusCode === 304) {
self.emit('warning', locale + ' translation for ' + docId + " wasn't modified. using the cache");
data = payload;
payload = null;
} else if (resp.statusCode === 404) {
self.emit('warning', 'Missing ' + locale + ' translation for ' + docId);
data = null;
} else if (resp.statusCode !== 200) {
throw new Error(errmsg(td.httpStatusMessage(resp, data)));
} else {
payload = data;
}
if (data) {
if (label)
console.timeEnd(label);
Object.keys(data).forEach((k) => potObj[k] = data[k]);
Gettext.json2po(potObj)
.then((dd) => {
if (payload) {
etag
.add(new ETagCache()
.setDocId(docId)
.setVersionId(versionId)
.setLanguage(locale)
.setPayload(payload)
.setServerETag(td.getETag()))
.save();
}
callback(null, {name: docId, type: 'po', locale: locale, data: dd, pulled: true});
})
.catch((ee) => callback(ee));
} else {
callback();
}
})
.catch((e) => callback(e));
return self;
}
/**
* Pull the translated document from the Zanata
*
* @param {string} projectId - the project id
* @param {string} versionId - the project version id
* @param {string} docId - the document id
* @param {string[]|string} locales - the locales you want to pull the translations
* @param {object} params - the parameters to pull the translated document from the Zanata
* @param {string} [params.potdir='./po'] - the place where store the cache file for source document
* @param {string} [params.podir='./po'] - the place where store the cache file for translated document
* @param {number} [params.verbose=0] - Show more progress messages verbosely.
* @param {boolean} [params.force=false] - Transfer the document without the cache
* @param {boolean} [params.skeletons=false] - Create skeleton entries even if the document isn't translated.
*
* @fires module:zanata/project~Project#fail
* @fires module:zanata/project~Project#warning
* @fires module:zanata/project~Project#data_pull
* @fires module:zanata/project~Project#end_pull
*/
pullTranslation(projectId, versionId, docId, locales, params) {
let self = this;
let result = [];
let potdir = params && params.potdir || './po';
let podir = params && params.podir || './po';
let setag = new ETag(path.join(potdir, 'etag-cache.json'));
let tetag = potdir === podir ? setag : new ETag(path.join(podir, 'etag-cache.json'));
if (projectId == undefined)
projectId = self.config.get('project');
if (versionId == undefined)
versionId = self.config.get('project-version');
if (typeof locales === 'string')
locales = [locales];
this.__pullSourceCb(projectId, versionId, docId, setag, params, (e, d) => {
if (e) {
self.emit('fail', e);
} else {
let fLocales;
if (locales) {
fLocales = () => {
return Promise.resolve(locales);
};
} else {
fLocales = () => {
let pil = new pilr(self.config.get('config.url'));
return pil
.setAuthUser(self.config.get('username'))
.setAuthToken(self.config.get('api-key'))
.get(projectId, versionId)
.then((dd) => {
let resp = dd.response;
let data = dd.json || dd.data;
if (resp.statusCode === 404)
throw new Error('No such projects or versions available on the server: [project: ' + projectId + ', version; ' + versionId + ']');
else if (resp.statusCode !== 200)
throw new Error(errmsg(pil.httpStatusMessage(resp, data)));
return data.map((o) => o.localeId);
});
};
}
let f = (vv) => {
return new Promise((resolve, reject) => {
self._pullTranslationCb(projectId, versionId, docId, d, vv, tetag, params, (ee, dd) => {
if (ee)
reject(ee);
else {
if (dd)
self.emit('data_pull', dd);
resolve({name: docId, type: 'po', locale: vv, pulled: dd != undefined});
}
});
});
};
fLocales()
.then((v) => {
return co(function* () {
let result = [];
for (let i = 0; i < v.length; i++) {
result.push(yield f(v[i]));
}
return result;
});
})
.then((v) => self.emit('end_pull', v))
.catch((e) => self.emit('fail', e));
}
});
return self;
}
/**
* Pull the translated documents from the Zanata
*
* @param {string} projectId - the project id
* @param {string} versionId - the project version id
* @param {string[]|string} locales - the locales you want to pull the translations
* @param {object} params - the parameters to pull the translated document from the Zanata
* @param {string} [params.potdir='./po'] - the place where store the cache file for source document
* @param {string} [params.podir='./po'] - the place where store the cache file for translated document
* @param {number} [params.verbose=0] - Show more progress messages verbosely.
* @param {boolean} [params.force=false] - Transfer the document without the cache
* @param {boolean} [params.skeletons=false] - Create skeleton entries even if the document isn't translated.
*
* @fires module:zanata/project~Project#fail
* @fires module:zanata/project~Project#warning
* @fires module:zanata/project~Project#data_pull
* @fires module:zanata/project~Project#end_pull
*/
pullTranslations(projectId, versionId, locales, params) {
let self = this;
let sd = new sdr(self.config.get('config.url'));
let result = [];
let fLocales;
let potdir = params && params.potdir || './po';
let podir = params && params.podir || './po';
let setag = new ETag(path.join(potdir, 'etag-cache.json'));
let tetag = potdir === podir ? setag : new ETag(path.join(podir, 'etag-cache.json'));
if (projectId == undefined)
projectId = self.config.get('project');
if (versionId == undefined)
versionId = self.config.get('project-version');
if (typeof locales === 'string')
locales = [locales];
if (locales) {
fLocales = () => Promise.resolve(locales);
} else {
fLocales = () => {
let pil = new pilr(self.config.get('config.url'));
return pil
.setAuthUser(self.config.get('username'))
.setAuthToken(self.config.get('api-key'))
.get(projectId, versionId)
.then((dd) => {
let resp = dd.response;
let data = dd.json || dd.data;
if (resp.statusCode === 404)
throw new Error('No such projects or versions available on the server: [project: ' + projectId + ', version: ' + versionId + ']');
else if (resp.statusCode !== 200)
throw new Error(errmsg(pil.httpStatusMessage(resp, data)));
return data.map((o) => o.localeId);
});
};
}
let pLocales = fLocales();
sd
.setAuthUser(self.config.get('username'))
.setAuthToken(self.config.get('api-key'))
.get(projectId, versionId)
.then((d) => {
let resp = d.response;
let data = d.json || d.data;
if (resp.statusCode === 404)
throw new Error('No such projects or versions available: [project: ' + projectId + ', version: ' + versionId + ']');
else if (resp.statusCode !== 200)
throw new Error(errmsg(sd.httpStatusMessage(resp, data)));
// obtains sources first
let fps = (doc) => {
return new Promise((resolve, reject) => {
self.__pullSourceCb(projectId, versionId, doc, setag, params, (ee, dd) => {
if (ee)
reject(ee);
else
resolve(dd);
});
});
};
return co(function* () {
let result = [];
for (let i = 0; i < data.length; i++) {
yield fps(data[i].name)
.then((dd) => {
result.push({name: data[i].name, data: dd});
})
.catch((ee) => Promise.reject(ee));
}
return result;
});
})
.then((d) => {
// pulling translations
let fpt = (doc, body, loc) => {
return new Promise((resolve, reject) => {
self._pullTranslationCb(projectId, versionId, doc, body, loc, tetag, params, (ee, dd) => {
if (ee)
reject(ee);
else {
if (dd)
self.emit('data_pull', dd);
resolve({name: doc, type: 'po', locale: loc, pulled: dd != undefined});
}
});
});
};
return fLocales()
.then((locs) => {
return co(function* () {
let result = [];
for (let i = 0; i < d.length; i++) { // sources
for (let j = 0; j < locs.length; j++) { // locales
yield fpt(d[i].name, d[i].data, locs[j])
.then((dd) => result.push(dd))
.catch((ee) => {
throw ee;
});
}
}
return result;
});
});
})
.then((result) => self.emit('end_pull', result))
.catch((e) => self.emit('fail', e));
return self;
}
/**
* Pull the documents from the Zanata
*
* @param {object} params - the parameters to pull the documents from the Zanata
* @param {string} [params.project] - the project id
* @param {string} [params.version] - the project version id
* @param {string} [params.pullType='both'] - what the type of document you want to pull. 'source' for the source document, 'trans' for the translated document and 'both'.
* @param {string|string[]} [params.locales] - the locale names you want to pull. this only takes effect when pullType is 'both' or 'trans'.
* @param {string} [params.potdir='./po'] - the place where store the cache file for source document
* @param {string} [params.podir='./po'] - the place where store the cache file for translated document
* @param {number} [params.verbose=0] - Show more progress messages verbosely.
* @param {boolean} [params.force=false] - Transfer the document without the cache
* @param {boolean} [params.skeletons=false] - Create skeleton entries even if the document isn't translated.
*
* @fires module:zanata/project~Project#fail
* @fires module:zanata/project~Project#warning
* @fires module:zanata/project~Project#data_pull
* @fires module:zanata/project~Project#end_pull
*/
pull(params) {
let self = this;
if (!params || !params.pullType || params.pullType.toLowerCase() === 'both') {
let ps = new Project(self.params);
let pt = new Project(self.params);
// connect on events
[ps, pt].forEach((v) => {
v
.on('warning', (e) => self.emit('warning', e))
.on('data_pull', (d) => self.emit('data_pull', d));
});
co(function* () {
let rs = yield new Promise((resolve, reject) => {
ps
.on('fail', (e) => reject(e))
.on('end_pull', (r) => resolve(r))
.pullSources(params.project, params.version, params);
});
let rt = yield new Promise((resolve, reject) => {
pt
.on('fail', (e) => reject(e))
.on('end_pull', (r) => resolve(r))
.pullTranslations(params.project, params.version, params.locales, params);
});
return [rs, rt];
})
.then((v) => self.emit('end_pull', _.flatten(v)))
.catch((e) => self.emit('fail', e));
} else if (params.pullType.toLowerCase() === 'source') {
self.pullSources(params.project, params.version, params);
} else if (params.pullType.toLowerCase() === 'trans') {
self.pullTranslations(params.project, params.version, params.locales, params);
} else {
self.emit('fail', new Error('Unknown pull type: ' + params.pullType));
}
return self;
}
_pushSourceCb(projectId, versionId, file, params, callback) {
let self = this;
let sd = new sdr(self.config.get('config.url'));
if (projectId == undefined)
projectId = self.config.get('project');
if (versionId == undefined)
versionId = self.config.get('project-version');
let verbose = params && params.verbose || false;
let contents;
try {
contents = fs.readFileSync(file, 'UTF-8');
} catch(e) {
callback(e);
return self;
}
if (contents == undefined || contents.length === 0) {
callback(new Error('No contents in ' + file));
} else {
let name = path.basename(file, '.pot');
Gettext.po2json(name, 'pot', contents)
.then((data) => {
let label;
if (verbose)
label = util.format('Uploading %s', file);
if (label)
console.time(label);
return sd
.setAuthUser(self.config.get('username'))
.setAuthToken(self.config.get('api-key'))
.putDoc(projectId, versionId, name, data)
.then((d) => {
let resp = d.response;
let data = d.json || d.data;
if (resp.statusCode === 404)
throw new Error('No such projects or versions available on the server: [project: ' + projectId + ', version: ' + versionId + ']');
else if (resp.statusCode === 403)
throw new Error('Project or Version might be in Read-Only mode: [project: ' + projectId + ', version: ' + versionId + ']');
else if (resp.statusCode === 401)
throw new Error('Unauthorized operation');
else if (resp.statusCode !== 200 && resp.statusCode !== 201)
throw new Error(sd.httpStatusMessage(resp, data));
if (label)
console.timeEnd(label);
callback(null, file);
});
})
.catch((e) => callback(e));
}
return self;
}
/**
* Push the source document to the Zanata
*
* @param {string} projectId - the project id
* @param {string} versionId - the project version id
* @param {string} file - the filename which you want to push as the source document
* @param {object} [params] - the parameters to push the source document
* @param {number} [params.verbose=0] - Show more progress messages verbosely.
*
* @fires module:zanata/project~Project#fail
* @fires module:zanata/project~Project#data_push
* @fires module:zanata/project~Project#end_push
*/
pushSource(projectId, versionId, file, params) {
let self = this;
self._pushSourceCb(projectId, versionId, file, params, (e, d) => {
if (e) {
self.emit('fail', e);
} else {
self.emit('data_push', d);
// for compatibility to other APIs
self.emit('end_push', [{docId: path.basename(file, '.pot'), type: 'pot', file: file}]);
}
});
return self;
}
/**
* data event from push methods. this event is emitted after pushing data to the Zanata.
* the passed data contains the filename that pushed to the Zanata.
*
* @event module:zanata/project~Project#data_push
* @type {string}
*/
/**
* end event from push methods. this event is emitted when all of pushing is finished.
*
* @event module:zanata/project~Project#end_push
* @type {object[]}
* @property {string} docId - the document id
* @property {string} type - the document type pushed. 'pot' for the source document and 'po' for the translated document
* @property {string} file - the finename pushed to the Zanata.
* @property {string} [locale] - the locale name pushed to the Zanata. this property is only available when pushing the translated document
*/
/**
* Push the source documents to the Zanata
*
* @param {string} projectId - the project id
* @param {string} versionId - the project version id
* @param {object} [params] - the parameters to push the source document
* @param {number} [params.verbose=0] - Show more progress messages verbosely.
* @param {string} [params.potdir='./po'] - the place where read POT files
* @param {boolean} [params.dryrun=false] - Do not send the data to the Zanata if true.
*
* @fires module:zanata/project~Project#fail
* @fires module:zanata/project~Project#data_push
* @fires module:zanata/project~Project#end_push
*/
pushSources(projectId, versionId, params) {
let self = this;
if (projectId == undefined)
projectId = self.config.get('project');
if (versionId == undefined)
versionId = self.config.get('project-version');
let verbose = params && params.verbose || false;
let potdir = params && params.potdir || './po';
let dryrun = params && params.dryrun || false;
let gm = new Glob(path.join(potdir, '**', '*.pot'), {nodir: true, dot: false})
.on('error', (err) => {
gm.abort();
self.emit('fail', err);
})
.on('end', (matches) => {
let f;
if (dryrun) {
f = (v) => {
let vv = path.basename(v, '.pot');
return Promise.resolve({docId: vv, type: 'pot', file: v});
};
} else {
f = (v) => {
let vv = path.basename(v, '.pot');
return new Promise((resolve, reject) => {
self._pushSourceCb(projectId, versionId, v, params, (e, d) => {
if (e)
reject(e);
else {
self.emit('data_push', d);
resolve({docId: vv, type: 'pot', file: v});
}
});
});
};
}
co(function* () {
let result = [];
for (let i = 0; i < matches.length; i++) {
result.push(yield f(matches[i]));
}
return result;
})
.then((v) => self.emit('end_push', v))
.catch((e) => self.emit('fail', e));
});
}
_pushTranslationCb(projectId, versionId, docId, locale, file, params, callback) {
let self = this;
if (projectId == undefined)
projectId = self.config.get('project');
if (versionId == undefined)
versionId = self.config.get('project-version');
let verbose = params && params.verbose || false;
let copyTrans = params && params.copyTrans || false;
let contents;
try {
contents = fs.readFileSync(file, 'UTF-8');
} catch(e) {
callback(e);
return self;
}
if (contents == undefined || contents.length === 0) {
callback(new Error('No contents in ' + file));
} else {
Gettext.po2json(docId, 'po', contents)
.then((data) => {
let label;
let td = new tdr(self.config.get('config.url'));
if (verbose)
label = 'Uploading ' + file;
if (label)
console.time(label);
return td
.setAuthUser(self.config.get('username'))
.setAuthToken(self.config.get('api-key'))
.put(projectId, versionId, docId, locale, data, copyTrans)
.then((d) => {
let resp = d.response;
let data = d.json || d.data;
if (resp.statusCode === 404)
throw new Error('No such projects, versions, or documents available on the server: [project: ' + projectId + ', version: ' + versionId + ', doc: ' + docId + ']');
else if (resp.statusCode === 401)
throw new Error('Unauthorized operation');
else if (resp.statusCode !== 200)
throw new Error(errmsg(td.httpStatusMessage(resp, data)));
if (label)
console.timeEnd(label);
callback(null, file);
});
})
.catch((e) => callback(e));
}
return self;
}
/**
* Push the translated document to the Zanata
*
* @param {string} projectId - the project id
* @param {string} versionId - the project version id
* @param {string} docId - the document id
* @param {string} locale - the locale name pushing the document
* @param {string} file - the filename which you want to push as the translated document
* @param {object} [params] - the parameters to push the translated document
* @param {number} [params.verbose=0] - Show more progress messages verbosely.
* @param {boolean} [params.copyTrans=false] - Copy the latest translations from equivalent messages/documents in the Zanata if true
*
* @fires module:zanata/project~Project#fail
* @fires module:zanata/project~Project#data_push
* @fires module:zanata/project~Project#end_push
*/
pushTranslation(projectId, versionId, docId, locale, file, params) {
let self = this;
if (projectId == undefined)
projectId = self.config.get('project');
if (versionId == undefined)
versionId = self.config.get('project-version');
if (locale instanceof Array) {
self.emit('fail', new Error('Only single locale is supported. Use pushTranslations() instead.'));
return self;
}
self._pushTranslationCb(projectId, versionId, docId, locale, file, params, (e, d) => {
if (e) {
self.emit('fail', e);
} else {
self.emit('data_push', d);
self.emit('end_push', {
docId: docId,
locale: locale,
type: 'po',
file: file
});
}
});
return self;
}
/**
* Push the translated documents to the Zanata
*
* @param {string} projectId - the project id
* @param {string} versionId - the project version id
* @param {string[]} locales - the locale name pushing the documents
* @param {object} [params] - the parameters to push the translated documents
* @param {string} [params.project-type] - the project type
* @param {string} [params.podir='./po'] - the place where read PO files
* @param {string} [params.potdir='./po'] - the place for POT files
* @param {number} [params.verbose=0] - Show more progress messages verbosely.
* @param {boolean} [params.copyTrans=false] - Copy the latest translations from equivalent messages/documents in the Zanata if true
* @param {boolean} [params.dryrun=false] - Do not send the data to the Zanata if true.
*
* @fires module:zanata/project~Project#fail
* @fires module:zanata/project~Project#warning
* @fires module:zanata/project~Project#data_push
* @fires module:zanata/project~Project#end_push
*/
pushTranslations(projectId, versionId, locales, params) {
let self = this;
let sd = new sdr(self.config.get('config.url'));
if (projectId == undefined)
projectId = self.config.get('project');
if (versionId == undefined)
versionId = self.config.get('project-version');
let projectType = params && params.projectType || self.config.get('project-type');
let verbose = params && params.verbose || false;
let copyTrans = params && params.copyTrans || false;
let potdir = params && params.potdir || './po';
let podir = params && params.podir || './po';
let dryrun = params && params.dryrun || false;
let fLocales;
if (locales)
fLocales = () => Promise.resolve(locales);
else {
fLocales = () => {
let pil = new pilr(self.config.get('config.url'));
return pil
.setAuthUser(self.config.get('username'))
.setAuthToken(self.config.get('api-key'))
.get(projectId, versionId)
.then((dd) => {
let resp = dd.response;
let data = dd.json || dd.data;
if (resp.statusCode === 404)
throw new Error('No such projects or versions available on the server: [project: ' + projectId + ', version: ' + versionId + ']');
else if (resp.statusCode !== 200)
throw new Error(errmsg(pil.httpStatusMessage(resp, data)));
return data.map((o) => o.localeId);
});
};
}
sd
.setAuthUser(self.config.get('username'))
.setAuthToken(self.config.get('api-key'))
.get(projectId, versionId)
.then((d) => {
let resp = d.response;
let data = d.json || d.data;
if (resp.statusCode === 404)
throw new Error('No such projects or versions available on the server: [project: ' + projectId + ', version: ' + versionId + ']');
else if (resp.statusCode !== 200)
throw new Error(errmsg(sd.httpStatusMessage(resp, data)));
let fpt = (doc, loc, file) => {
return new Promise((resolve, reject) => {
self._pushTranslationCb(projectId, versionId, doc, loc, file, params, (ee, dd) => {
if (ee)
reject(ee);
else {
self.emit('data_push', dd);
resolve({docId: doc, locale: loc, type: 'po', file: file});
}
});
});
};
return fLocales()
.then((locs) => {
let fm = new fmr(self.config.get('rules'));
return co(function* () {
let result = [];
for (let i = 0; i < data.length; i++) { // docs
for (let j = 0; j < locs.length; j++) { // locales
let file = fm.getRealPath(projectType, {path: podir, locale: locs[j], filenamem: data[i].name, extension: 'po', source: path.join(potdir, data[i].name + '.pot')});
if (file) {
yield fpt(data[i].name, locs[j], file)
.then((dd) => result.push(d))
.catch((ee) => {
throw ee;
});
} else {
self.emit('warning', 'Unable to determine the file path: [doc: ' + data[i].name + ', locale: ' + locs[j] + ']');
}
}
}
return result;
});
});
})
.then((result) => self.emit('end_push', result))
.catch((e) => self.emit('fail', e));
return self;
}
/**
* Push the documents to the Zanata
*
* @param {object} [params] - the parameters to push the documents
* @param {string} [params.pushType='both'] - what the type of the document you want to push. 'source' for the source documents, 'trans' for the translated documents, and 'both'.
* @param {string} [params.project] - the project id
* @param {string} [params.version] - the project version id
* @param {string} [params.locales] - the locale names you want to push
* @param {string} [params.project-type] - the project type
* @param {string} [params.potdir='./po'] - the place where read POT files
* @param {string} [params.podir='./po'] - the place where read PO files
* @param {number} [params.verbose=0] - Show more progress messages verbosely.
* @param {boolean} [params.copyTrans=false] - Copy the latest translations from equivalent messages/documents in the Zanata if true
* @param {boolean} [params.dryrun=false] - Do not send the data to the Zanata if true.
*
* @fires module:zanata/project~Project#fail
* @fires module:zanata/project~Project#warning
* @fires module:zanata/project~Project#data_push
* @fires module:zanata/project~Project#end_push
*/
push(params) {
let self = this;
let fdryrun, frun;
if (!params || !params.pushType || params.pushType.toLowerCase() === 'both') {
let ps = new Project(self.params);
let pt = new Project(self.params);
// connect on events
[ps, pt].forEach((v) => {
v
.on('warning', (e) => self.emit('warning', e))
.on('data_push', (d) => self.emit('data_push', d));
});
co(function* () {
let rs = yield new Promise((resolve, reject) => {
ps
.on('fail', (e) => reject(e))
.on('end_push', (r) => resolve(r))
.pushSources(params.project, params.version, params);
});
let rt = yield new Promise((resolve, reject) => {
pt
.on('fail', (e) => reject(e))
.on('end_push', (r) => resolve(r))
.pushTranslations(params.project, params.version, params.locales, params);
});
return [rs, rt];
})
.then((v) => self.emit('end_push', _.flatten(v)))
.catch((e) => self.emit('fail', e));
} else if (params.pushType.toLowerCase() === 'source') {
self.pushSources(params.project, params.version, params);
} else if (params.pushType.toLowerCase() === 'trans') {
self.pushTranslations(params.project, params.version, params.locales, params);
} else {
self.emit('fail', new Error('Unknown push type: ' + params.pushType));
}
return self;
}
/**
* data event from stats method
*
* @event module:zanata/project~Project#data_stats
* @type {object}
* @property {string} id
* @property {object[]} stats
* @property {number} stats.total
* @property {number} stats.untranslated
* @property {number} stats.needReview
* @property {number} stats.translated
* @property {number} stats.approved
* @property {number} stats.rejected
* @property {number} stats.fuzzy
* @property {string} stats.unit
* @property {string} stats.locale
* @property {string} stats.lastTranslated
* @property {number} stats.translatedOnly
* @property {object[]} detailedStats
*
* @see {@link https://zanata.ci.cloudbees.com/job/zanata-api-site/site/zanata-common-api/rest-api-docs/json_TranslationStatistics.html|TranslationStatistics data type}
*/
/**
* Obtain document statistics from the Zanata
*
* @param {string} projectId - the project id
* @param {string} versionId - the project version id
* @param {string} [docId] - the document id
* @param {object} [params] - the parameters to obtain the document statistics
* @param {string[]|string} [params.locales] - the locale names you want to obtain the statistics
* @param {boolean} [params.detail=false] - whether include the detailed statistics
* @param {boolean} [params.word=false] - whether include the word-level statistics
*
* @fires module:zanata/project~Project#fail
* @fires module:zanata/project~Project#data_stats
*/
stats() {
let args = arguments;
let self = this;
// assume the first function argument should be a callback
let projectId = args[0] || self.config.get('project');
let versionId = args[1] || self.config.get('project-version');
let docId, username, dateRange, params;
let stat = new sr(self.config.get('config.url'))
.setAuthUser(self.config.get('username'))
.setAuthToken(self.config.get('api-key'));
if (args.length === 3) {
// Get translation statistics for a Project iteration and (optionally) it's underlying documents.
// projectId, versionId, params
params = args[2];
stat.get(projectId, versionId, params)
.then((d) => {
let resp = d.response;
let data = d.json || d.data;
if (resp.statuscode === 404)
throw new Error('No such projects or versions available on the server: [project: ' + projectId + ', version: ' + versionId + ']');
else if (resp.statusCode !== 200)
throw new Error(errmsg(stat.httpStatusMessage(resp, data)));
self.emit('data_stats', data);
})
.catch((e) => self.emit('fail', e));
} else if (args.length === 4) {
// Get translation statistics for a Document
// projectId, versionId, docId, params
docId = args[2];
params = args[3];
stat.getDoc(projectId, versionId, docId, params)
.then((d) => {
let resp = d.response;
let data = d.json || d.data;
if (resp.statusCode === 404)
throw new Error('No such projects, versions, or documents available on the server: [project: ' + projectId + ', version: ' + versionId + ', doc: ' + docId + ']');
else if (resp.statusCode !== 200)
throw new Error(errmsg(stat.httpStatusMessage(resp, data)));
self.emit('data_stats', data);
})
.catch((e) => self.emit('fail', e));
} else if (args.length === 5) {
// Get contribution statistic from project-version within given date range.
// projectId, versionId, username, dateRange, params
username = args[2];
dateRange = args[3];
params = args[4];
self.emit('fail', new Error('not yet supported'));
} else {
self.emit('fail', new Error('Invalid parameters'));
}
return this;
}
}
exports.Project = Project;