index.js had grown to 1,642 lines / ~50 functions. This peels the three biggest self-contained concerns out into their own modules and wires them back in through a dependency-bag factory so each module stays free of module-level mutable state. lib/webex.js — every webexapis.com round-trip (fetchWithRateLimit, whoAmI, findWebexGroup, getGroupMembers, getPersonInfo, findPersonByEmail, sendDirectMessage, sendMessageWithRetry, sendDirectCard, deleteMessage, refreshToken). Factory closes over token getters and the logger. lib/translation.js — Google Translate fan-out (buildTranslations, translateMessage). Reuses the webex 429 helper so the app has one retry policy. lib/jobs.js — cron-driven pipeline (checkScheduledJobs, processRunningQueue, sendQueueJobMessages, buildJobCompletedCard) plus the two recipient-resolution helpers (collectGroupMembers, buildPeopleList). Factory takes jobs/queue/userPrefs/webex/etc so mutable state stays owned by index.js. index.js: instantiates webex/translator/jobsPipeline once, rewires every call site to go through them, and drops ~715 lines of moved code plus a dead msToTime wrapper. Down from 1,642 → 969 lines, 26 top-level functions instead of 50+. Tests: 53/53 helpers still green. Smoke tested /info, /admin/users, /user/groups/list, /user/groups/find, /jobs/list/all, and the unauth 401 path — all match pre-R2 behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
84 lines
3.7 KiB
JavaScript
84 lines
3.7 KiB
JavaScript
// Google Translate wrappers used when a bot is configured to fan a message
|
|
// out into multiple languages. Splitting out into its own module keeps the
|
|
// google-translate API key (env-only) and the per-language plural-request
|
|
// dance in one place — index.js doesn't need to know about either.
|
|
//
|
|
// createTranslator({ languages, apiKey, fetchWithRateLimit, logger })
|
|
// returns { buildTranslations(message) }. Callers hand in the same
|
|
// fetchWithRateLimit that lib/webex.js uses so the whole app has a single
|
|
// 429 retry policy.
|
|
|
|
export function createTranslator(deps) {
|
|
var languages = Array.isArray(deps.languages) ? deps.languages : [];
|
|
var apiKey = deps.apiKey || '';
|
|
var fetchWithRateLimit = deps.fetchWithRateLimit;
|
|
var logger = deps.logger || function () {};
|
|
|
|
// POST to Google's translate v2 REST endpoint for one (message, target
|
|
// language, format) triple. Resolves to a normalized record so the
|
|
// fan-out caller can index by language name.
|
|
function translateMessage(message, language, format, messageType) {
|
|
return new Promise(function (resolve, reject) {
|
|
var params = {
|
|
q: message,
|
|
target: language.key,
|
|
format: format,
|
|
source: 'en',
|
|
model: 'nmt',
|
|
key: apiKey
|
|
};
|
|
var translateUrl = 'https://translation.googleapis.com/language/translate/v2?' + new URLSearchParams(params);
|
|
fetchWithRateLimit(translateUrl, { method: 'POST' })
|
|
.then(response => response.json())
|
|
.then(result => {
|
|
resolve({
|
|
name: language.name,
|
|
key: language.key,
|
|
format: format,
|
|
messageType: messageType,
|
|
translatedText: result.data.translations[0].translatedText
|
|
});
|
|
})
|
|
.catch(reject);
|
|
});
|
|
}
|
|
|
|
// Fans `message` out across every configured language in text +
|
|
// markdown + html variants. Returns an object shaped like:
|
|
// { english: {text, markdown, html}, "<languageName>": {...}, ... }
|
|
// Failed translations are silently dropped (partial success is better
|
|
// than the whole send blowing up when one language server is down).
|
|
function buildTranslations(message) {
|
|
return new Promise(function (resolve) {
|
|
logger('buildTranslations', 'Started');
|
|
var translationPromises = [];
|
|
var translations = {
|
|
english: {
|
|
text: message.text,
|
|
markdown: message.markdown,
|
|
html: message.html
|
|
}
|
|
};
|
|
|
|
for (var language of languages) {
|
|
translationPromises.push(translateMessage(message.text, language, 'text', 'text'));
|
|
translationPromises.push(translateMessage(message.markdown, language, 'text', 'markdown'));
|
|
translationPromises.push(translateMessage(message.html, language, 'html', 'html'));
|
|
}
|
|
|
|
Promise.allSettled(translationPromises).then(function (results) {
|
|
for (var translation of results) {
|
|
if (translation.status === 'fulfilled') {
|
|
var name = translation.value.name;
|
|
if (!translations[name]) translations[name] = {};
|
|
translations[name][translation.value.messageType] = translation.value.translatedText;
|
|
}
|
|
}
|
|
logger('buildTranslations', 'Completed');
|
|
resolve(translations);
|
|
});
|
|
});
|
|
}
|
|
|
|
return { buildTranslations, translateMessage };
|
|
}
|