'use strict';
/**
* @fileoverview Automated translation of i18n JSON files.
* Supports DeepL API and free MyMemory service with duplicate caching.
* @module translator
*/
const path = require('path');
const { readJsonFile, writeJsonFile, flattenJson, unflattenJson } = require('./json-utils');
async function prepareTranslationData(sourceLang, i18nDir) {
const sourceFile = path.join(i18nDir, `${sourceLang}.json`);
const sourceJson = await readJsonFile(sourceFile);
if (!sourceJson) {
throw new Error(`Source file not found or invalid: ${sourceFile}`);
}
const flat = flattenJson(sourceJson);
const entries = Object.entries(flat).filter(([, v]) => v && typeof v === 'string');
const keys = entries.map(([k]) => k);
const values = entries.map(([, v]) => v);
const uniqueValues = [...new Set(values)];
return { keys, values, uniqueValues };
}
function logTranslateHeader(ctx) {
const { sourceLang, targetLang, useDeepL, log } = ctx;
log('Transloco Auto-Translate');
log('='.repeat(50));
log(`Source: ${sourceLang}.json -> Target: ${targetLang}.json`);
log(`Provider: ${useDeepL ? 'DeepL' : 'MyMemory (free)'}`);
log();
}
function logTranslationSummary(ctx) {
const { valuesCount, failedCount, useDeepL, email, log } = ctx;
log('\nSummary');
log('-'.repeat(50));
log(`Strings translated: ${valuesCount - failedCount}/${valuesCount}`);
if (failedCount > 0) {
log(`Failed translations: ${failedCount} (kept original text)`);
}
log(`Provider: ${useDeepL ? 'DeepL API' : 'MyMemory (free)'}`);
if (!useDeepL && !email) {
log('Tip: Use --email=your@email.com for higher rate limits');
}
}
async function executeDeepLTranslation(ctx) {
const { uniqueValues, sourceLang, targetLang, provider } = ctx;
const translated = await provider.translateBatch({
texts: uniqueValues,
fromLang: sourceLang,
toLang: targetLang,
});
return {
translationMap: new Map(uniqueValues.map((v, i) => [v, translated[i]])),
failedCount: 0,
};
}
function executeMyMemoryTranslation(ctx) {
const { uniqueValues, sourceLang, targetLang, provider, email, verbose } = ctx;
return provider.translateBatch({
texts: uniqueValues,
fromLang: sourceLang,
toLang: targetLang,
options: {
email,
verbose,
onProgress: (processed, total) => {
if (processed < total) {
process.stdout.write(`\r Progress: ${processed}/${total}`);
}
},
},
});
}
function executeTranslation(ctx) {
return ctx.useDeepL ? executeDeepLTranslation(ctx) : executeMyMemoryTranslation(ctx);
}
async function saveTranslationResult(ctx) {
const { targetJson, targetFile, startTime, dryRun = false, log = console.log } = ctx;
if (dryRun) {
log('\n[DRY RUN] Would write:');
log(JSON.stringify(targetJson, null, 2));
return;
}
await writeJsonFile(targetFile, targetJson);
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
log(`\nTranslated: ${targetFile} (${elapsed}s)`);
}
function buildTranslatedOutput(ctx) {
const { keys, values, translationMap, i18nDir, targetLang } = ctx;
const translatedFlat = Object.fromEntries(
keys.map((k, i) => [k, translationMap.get(values[i]) ?? values[i]]),
);
return {
targetJson: unflattenJson(translatedFlat),
targetFile: path.join(i18nDir, `${targetLang}.json`),
};
}
function logDuplicateInfo(ctx) {
const { values, uniqueValues, verbose, log } = ctx;
if (values.length > uniqueValues.length && verbose) {
log(` ${values.length - uniqueValues.length} duplicate strings will use cache`);
}
}
async function performTranslation(ctx) {
const { uniqueValues, sourceLang, targetLang, provider, useDeepL, email, verbose, log } = ctx;
log(`Translating ${uniqueValues.length} unique strings...`);
const result = await executeTranslation({
uniqueValues,
sourceLang,
targetLang,
provider,
useDeepL,
email,
verbose,
});
log();
return result;
}
function handleEmptyTranslation(log) {
log('No strings to translate.');
return { success: true, translated: 0 };
}
function buildTranslationContext(ctx) {
const { uniqueValues, sourceLang, targetLang, provider, useDeepL, email, verbose, log } = ctx;
return { uniqueValues, sourceLang, targetLang, provider, useDeepL, email, verbose, log };
}
function buildOutputContext(ctx, translationMap) {
const { keys, values, i18nDir, targetLang } = ctx;
return { keys, values, translationMap, i18nDir, targetLang };
}
async function runTranslation(ctx) {
logDuplicateInfo(ctx);
const startTime = Date.now();
const { translationMap, failedCount } = await performTranslation(buildTranslationContext(ctx));
const { targetJson, targetFile } = buildTranslatedOutput(buildOutputContext(ctx, translationMap));
await saveTranslationResult({
targetJson,
targetFile,
startTime,
dryRun: ctx.dryRun,
log: ctx.log,
});
logTranslationSummary({
valuesCount: ctx.values.length,
failedCount,
useDeepL: ctx.useDeepL,
email: ctx.email,
log: ctx.log,
});
return { success: true, translated: ctx.values.length - failedCount, failed: failedCount };
}
function parseTranslateOptions(options) {
const {
i18nDir,
provider,
useDeepL = false,
email,
verbose = false,
dryRun = false,
log = console.log,
} = options;
return { i18nDir, provider, useDeepL, email, verbose, dryRun, log };
}
/**
* Translates a JSON language file using DeepL or MyMemory
* @param {string} sourceLang - Source language code (e.g., 'en')
* @param {string} targetLang - Target language code (e.g., 'fr')
* @param {TranslateOptions} [options]
* @returns {Promise<TranslateResult>}
* @example
* await translateFile('en', 'fr', { i18nDir: './src/i18n', provider: deepLProvider });
*/
async function translateFile(sourceLang, targetLang, options = {}) {
const opts = parseTranslateOptions(options);
logTranslateHeader({ sourceLang, targetLang, useDeepL: opts.useDeepL, log: opts.log });
const data = await prepareTranslationData(sourceLang, opts.i18nDir);
if (data.keys.length === 0) {
return handleEmptyTranslation(opts.log);
}
return runTranslation({ ...data, sourceLang, targetLang, ...opts });
}
module.exports = {
translateFile,
};