collabcentral/lib/translation.js
Joseph B. McQueen 8c3a32f47d Phase R2: extract lib/webex, lib/translation, lib/jobs from index.js
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>
2026-07-02 08:33:42 -04:00

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 };
}