i18n-llm-translate
Version:
Automatically translates namespace-based JSON translation files across multiple languages from any source language
130 lines (129 loc) • 7.16 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.translate = translate;
const cache_1 = require("./cache");
const cleaner_1 = require("./cleaner");
const namespace_1 = require("./namespace");
const util_1 = require("./util");
const zod_1 = require("zod");
function translate(engine, options) {
return __awaiter(this, void 0, void 0, function* () {
// We filter out base language, as it is used only as reference
options.targetLanguageCodes = options.targetLanguageCodes.filter(languageCode => languageCode !== options.baseLanguageCode);
// We operate only on json cache
if (!options.namesMapping)
options.namesMapping = {};
if (!options.namesMapping.jsonCache)
options.namesMapping.jsonCache = '.translations-cache';
if (!options.namesMapping.jsonCache.endsWith(".json"))
options.namesMapping.jsonCache = `${options.namesMapping.jsonCache}.json`;
// Fix application context. Each entry is treated as separate sentence
for (let i = 0; i < options.applicationContextEntries.length; i++) {
let entry = options.applicationContextEntries[i].trim();
if (!entry.endsWith("."))
entry = `${entry}.`;
options.applicationContextEntries[i] = entry;
}
const namespaces = yield (0, namespace_1.readTranslationsNamespaces)(options);
const cache = yield (0, cache_1.readTranslationsCache)(options);
if (options.cleanup) {
// Remove files that are neither language directories nor cache file
yield (0, cleaner_1.cleanLanguagesDirectory)(options);
// Clean each language directory by removing files that are not namespace files from the base language directory
yield (0, cleaner_1.cleanNamespaces)(options, namespaces);
}
const dirtyCache = cache.cleanCache(namespaces);
// Fix cache structure so it matches all paths with base namespaces
cache.syncCacheWithNamespaces(namespaces, false);
console.log(`Translation# Using engine: "${engine.name}"`);
let dirty = false;
for (let namespace of namespaces) {
const baseDifferences = cache.getBaseLanguageTranslationDifferences(namespace);
if (baseDifferences) {
dirty = true;
const baseDifferencesSchema = generateTranslationsZodSchema(baseDifferences);
const engineResultSchema = generateLanguagesTranslateReturnZodSchema(options.targetLanguageCodes, baseDifferencesSchema);
console.log(`Translation# Translating base differences for namespace: "${namespace.jsonFileName}".`);
const translationsResults = yield engine.translate(baseDifferences, options);
const engineCheck = engineResultSchema.safeParse(translationsResults);
if (!engineCheck.success) {
(0, util_1.logWithColor)('red', `Translation# Engine does not returned proper translation structure!`);
(0, util_1.logWithColor)('red', `Translation# Base differences:`, baseDifferences);
(0, util_1.logWithColor)('red', `Translation# Validation error:`, engineCheck.error.issues);
return;
}
(0, namespace_1.applyEngineTranslations)(namespace, translationsResults);
}
}
// Find missing translations
for (let namespace of namespaces) {
const missed = namespace.getMissingTranslations();
if (missed) {
dirty = true;
// "Missed" translations are already structured according to their respective languages.
// Different languages may have varying missing translations,
// unlike differential translations based on the primary language.
const engineResultSchema = generateTranslationsZodSchema(missed.targetLanguageTranslationsKeys);
console.log(`Translation# Translating missed translations for namespace: "${namespace.jsonFileName}".`);
if (options.debug)
console.log(`Translation# Missed translations`); //: `, JSON.stringify(missed, null, 2));
// `baseLanguageTranslations` contains merged missing translations, regardless of the language,
// while avoiding duplicates. This reduces the required context, leading to lower token consumption.
const translationsResults = yield engine.translateMissed(missed, options);
const engineCheck = engineResultSchema.safeParse(translationsResults);
if (!engineCheck.success) {
(0, util_1.logWithColor)('red', `Translation# Engine does not returned proper translation structure!`);
(0, util_1.logWithColor)('red', `Translation# Validation error:`, engineCheck.error.issues);
return;
}
(0, namespace_1.applyEngineTranslations)(namespace, translationsResults);
}
}
if (dirty || dirtyCache) {
// Overwrite cache with namespace values
if (dirty)
cache.syncCacheWithNamespaces(namespaces, true);
yield cache.write();
}
if (dirty) {
for (let namespace of namespaces) {
yield namespace.write();
}
(0, util_1.logWithColor)('green', `Translation# Translated and saved successfully`);
}
else {
(0, util_1.logWithColor)('green', `Translation# No changes detected.`);
}
});
}
function generateLanguagesTranslateReturnZodSchema(targetLanguages, differencesSchema) {
const object = {};
for (let targetLanguage of targetLanguages) {
object[targetLanguage] = differencesSchema;
}
return zod_1.z.object(object);
}
function generateTranslationsZodSchema(obj) {
const object = {};
for (let [key, value] of Object.entries(obj)) {
if (typeof value === "string") {
object[key] = zod_1.z.string();
}
else if (typeof value === "object" && value !== null) {
object[key] = generateTranslationsZodSchema(value);
}
else {
throw new Error(`Invalid type for key '${key}': Expected string or object.`);
}
}
return zod_1.z.object(object);
}