UNPKG

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
"use strict"; 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); }