const fs = require('fs'); const path = require('path'); const mapCache = new Map(); function buildCanonical(entry, parsedPayload, connector) { const translator = entry && typeof entry.translator === 'object' ? entry.translator : {}; const canonical = { ...parsedPayload }; if (translator.forceInstrumentId !== false) { canonical.instrument_id = entry.instrument_id; } canonical.meta = { ...(parsedPayload.meta || {}), ...(translator.meta && typeof translator.meta === 'object' ? translator.meta : {}), connector, instrument_config: entry.config }; return canonical; } function resolveTranslatorFilePath(filePath, configFilePath) { if (!filePath || typeof filePath !== 'string') return ''; if (path.isAbsolute(filePath)) return filePath; const candidates = [ path.resolve(process.cwd(), filePath) ]; if (configFilePath) { candidates.push(path.resolve(path.dirname(configFilePath), filePath)); } const matched = candidates.find((candidate) => fs.existsSync(candidate)); return matched || candidates[0]; } function parseMapFile(fileContent, filePath) { const lines = fileContent.split(/\r?\n/); const rows = new Map(); lines.forEach((line, index) => { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) return; const separator = line.indexOf('='); if (separator < 0) { throw new Error(`${filePath}:${index + 1} invalid mapping line (expected KEY = value)`); } const key = line.slice(0, separator).trim(); const value = line.slice(separator + 1).trim(); if (!key) { throw new Error(`${filePath}:${index + 1} mapping key is required`); } rows.set(key, value); }); return rows; } function loadMapFile(filePath) { const stat = fs.statSync(filePath); const cached = mapCache.get(filePath); if (cached && cached.mtimeMs === stat.mtimeMs) { return cached.rows; } const content = fs.readFileSync(filePath, 'utf8'); const rows = parseMapFile(content, filePath); mapCache.set(filePath, { mtimeMs: stat.mtimeMs, rows }); return rows; } function getPlaceholderValue(name, context) { if (Object.hasOwn(context.flat, name)) { return context.flat[name]; } if (!name.includes('.')) { return ''; } const parts = name.split('.').filter(Boolean); let current = context.root; for (let i = 0; i < parts.length; i += 1) { if (!current || typeof current !== 'object') return ''; current = current[parts[i]]; } return current === undefined || current === null ? '' : current; } function renderTemplate(template, context) { return String(template).replace(/\{([^{}]+)\}/g, (_, rawName) => { const name = String(rawName || '').trim(); if (!name) return ''; const value = getPlaceholderValue(name, context); return value === undefined || value === null ? '' : String(value); }); } function buildTemplateContext(entry, parsedPayload, connector) { const root = { ...parsedPayload, instrument_id: parsedPayload.instrument_id || entry.instrument_id, connector, config: entry.config || {}, meta: parsedPayload.meta || {} }; const flat = { ...root, ...(root.meta && typeof root.meta === 'object' ? root.meta : {}), ...(root.config && typeof root.config === 'object' ? root.config : {}) }; if (Array.isArray(parsedPayload.results)) { flat.order_tests = parsedPayload.results .map((item) => item && item.test_code) .filter(Boolean) .map((testCode) => `^^^${testCode}`) .join('\\'); } return { root, flat }; } function translateOverrides(entry, parsedPayload, connector) { const translator = entry && typeof entry.translator === 'object' ? entry.translator : {}; const overrides = translator.overrides && typeof translator.overrides === 'object' ? translator.overrides : {}; const canonical = buildCanonical(entry, { ...parsedPayload, ...overrides }, connector); return canonical; } function translateTemplate(entry, parsedPayload, connector) { const translator = entry && typeof entry.translator === 'object' ? entry.translator : {}; if (!translator.file || typeof translator.file !== 'string') { throw new Error('translator.file is required for template engine'); } const resolvedFilePath = resolveTranslatorFilePath(translator.file, entry?.files?.config); if (!fs.existsSync(resolvedFilePath)) { throw new Error(`translator file not found: ${translator.file}`); } const mapRows = loadMapFile(resolvedFilePath); const messageKeys = Array.isArray(translator.messages) && translator.messages.length ? translator.messages.map((value) => String(value)) : Array.from(mapRows.keys()); const context = buildTemplateContext(entry, parsedPayload, connector); const renderedMessages = messageKeys.map((messageKey) => { if (!mapRows.has(messageKey)) { throw new Error(`translator message key not found in map file: ${messageKey}`); } return { key: messageKey, body: renderTemplate(mapRows.get(messageKey), context) }; }); const canonical = buildCanonical(entry, parsedPayload, connector); canonical.meta.rendered_messages = renderedMessages; canonical.meta.translator_file = resolvedFilePath; return canonical; } const registry = new Map([ ['overrides', { translate: translateOverrides }], ['template', { translate: translateTemplate }] ]); function resolve(name) { if (!name) return registry.get('overrides'); const key = String(name).trim().toLowerCase(); return registry.get(key) || null; } function translate(entry, parsedPayload, connector, engineName) { const engine = resolve(engineName); if (!engine) { const options = engineName ? ` (requested: ${engineName})` : ''; throw new Error(`translator engine not found${options}`); } return engine.translate(entry, parsedPayload, connector); } module.exports = { resolve, translate };