UNPKG

i18n-llm-translate

Version:

Automatically translates namespace-based JSON translation files across multiple languages from any source language

239 lines (238 loc) 12.7 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()); }); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.readTranslationsNamespaces = readTranslationsNamespaces; exports.applyEngineTranslations = applyEngineTranslations; const util_1 = require("./util"); const path_1 = __importDefault(require("path")); const fs_1 = require("fs"); function validateTranslationStructure(content) { if (typeof content !== 'object' || content === null || Array.isArray(content)) { throw new Error('Translation# Translation content must be an object with string values or nested objects. Arrays are not allowed.'); } for (const key of Object.keys(content)) { if (key.includes('.')) { throw new Error(`Translation# Invalid key '${key}': Keys cannot contain dots.`); } if (typeof content[key] === 'object' && content[key] !== null) { validateTranslationStructure(content[key]); } else if (typeof content[key] !== 'string') { throw new Error(`Translation# Invalid value for key '${key}': Only strings are allowed as values.`); } else if (!content[key].trim()) { throw new Error(`Translation# Invalid value for key '${key}': Values cannot be empty strings`); } } } // Remove unused keys inside of target translations function cleanTargetTranslations(baseContent, targetContent) { if (typeof baseContent !== 'object' || typeof targetContent !== 'object') { return targetContent; } const cleanedContent = {}; for (const key of Object.keys(baseContent)) { if (key in targetContent) { cleanedContent[key] = cleanTargetTranslations(baseContent[key], targetContent[key]); } } return cleanedContent; } function readTranslationsNamespaces(options) { return __awaiter(this, void 0, void 0, function* () { const languagesDirectory = path_1.default.resolve(process.env.PWD, options.languagesDirectoryPath); const baseLanguageDirectory = path_1.default.resolve(languagesDirectory, (0, util_1.formatLanguageContainerDirectoryName)(options.baseLanguageCode, options)); const namespaces = []; const namespaceFiles = yield fs_1.promises.readdir(baseLanguageDirectory); for (const fileName of namespaceFiles) { if (!fileName.endsWith('.json')) { continue; } try { const filePath = path_1.default.join(baseLanguageDirectory, fileName); let content = yield fs_1.promises.readFile(filePath, 'utf-8'); try { content = JSON.parse(content); } catch (error) { // noinspection ExceptionCaughtLocallyJS throw new Error('File is not a proper JSON!'); } namespaces.push({ jsonFileName: fileName, baseLanguageTranslations: content, targetLanguages: [], write() { return __awaiter(this, void 0, void 0, function* () { const savedLanguages = []; for (let targetLanguageCode of options.targetLanguageCodes) { const targetLanguage = this.targetLanguages .find(t => t.languageCode === targetLanguageCode); if (!targetLanguage.dirty) continue; const targetFilePath = path_1.default.join(languagesDirectory, (0, util_1.formatLanguageContainerDirectoryName)(targetLanguageCode, options), fileName); try { let targetContent = targetLanguage.translations; yield fs_1.promises.writeFile(targetFilePath, JSON.stringify(targetContent, null, 4), 'utf-8'); savedLanguages.push(targetLanguageCode); } catch (error) { console.error(`Translation# Error writing to ${targetFilePath}:`, error); } } if (savedLanguages.length) { const languages = savedLanguages.length === 1 ? savedLanguages[0] : `(\n ${savedLanguages.join(', ')}\n)`; console.log(`Translation# Successfully wrote translations to ${languagesDirectory}/${languages}/${fileName}`); } }); }, getMissingTranslations() { function walk(baseTranslations, targetLanguagesTranslations) { const x = { baseLanguageTranslations: {}, targetLanguageTranslationsKeys: {} }; function getTargetContainer(languageCode) { let missingKeys = x.targetLanguageTranslationsKeys[languageCode]; if (!missingKeys) { missingKeys = {}; x.targetLanguageTranslationsKeys[languageCode] = missingKeys; } return missingKeys; } for (const [key, value] of Object.entries(baseTranslations)) { if (typeof value === "object") { // Dig target languages const targetDig = []; for (let _a of targetLanguagesTranslations) { let { translations } = _a, rest = __rest(_a, ["translations"]); targetDig.push(Object.assign({ translations: translations[key] || {} }, rest)); } const result = walk(value, targetDig); if (result) { x.baseLanguageTranslations[key] = result.baseLanguageTranslations; for (let languageCode in result.targetLanguageTranslationsKeys) { getTargetContainer(languageCode)[key] = result.targetLanguageTranslationsKeys[languageCode]; } } continue; } let foundMissed = false; for (let languageContainer of targetLanguagesTranslations) { if (!languageContainer.translations[key]) { foundMissed = true; // Empty string, we need it just to create schema getTargetContainer(languageContainer.languageCode)[key] = ''; } } if (!foundMissed) { continue; } x.baseLanguageTranslations[key] = value; } if (Object.keys(x.baseLanguageTranslations).length === 0) return undefined; return x; } return walk(this.baseLanguageTranslations, this.targetLanguages); } }); } catch (error) { console.error(`Translation# Error reading "${baseLanguageDirectory}/${fileName}":`); throw error; } } for (let namespace of namespaces) { try { validateTranslationStructure(namespace.baseLanguageTranslations); } catch (error) { console.log(`Translation# Translation namespace "${baseLanguageDirectory}/${namespace.jsonFileName} not valid:`); throw error; } } for (let targetLanguageCode of options.targetLanguageCodes) { const targetLanguageDirectory = path_1.default.resolve(languagesDirectory, (0, util_1.formatLanguageContainerDirectoryName)(targetLanguageCode, options)); try { yield fs_1.promises.access(targetLanguageDirectory); } catch (error) { console.log(`Translation# Creating directory for target language: ${targetLanguageCode}`); yield fs_1.promises.mkdir(targetLanguageDirectory, { recursive: true }); } for (const namespace of namespaces) { try { const filePath = path_1.default.join(targetLanguageDirectory, namespace.jsonFileName); let content; let contentString = '{}'; try { contentString = yield fs_1.promises.readFile(filePath, 'utf-8'); } catch (error) { (0, util_1.logWithColor)('yellow', `Translation# Warning: "${targetLanguageCode}/${namespace.jsonFileName}" not found. Initializing empty JSON.`); } try { content = JSON.parse(contentString); } catch (error) { (0, util_1.logWithColor)('yellow', `Translation# Warning: "${targetLanguageCode}/${namespace.jsonFileName}" invalid JSON.`); // noinspection ExceptionCaughtLocallyJS throw error; } namespace.targetLanguages.push({ dirty: false, languageCode: targetLanguageCode, translations: cleanTargetTranslations(namespace.baseLanguageTranslations, content), }); } catch (error) { console.error(`Translation# Error reading "${targetLanguageCode}/${namespace.jsonFileName}":`); throw error; } } } return namespaces; }); } function applyEngineTranslations(namespace, engineTranslations) { for (let languageCode of Object.keys(engineTranslations)) { const targetLanguage = namespace.targetLanguages.find(lang => lang.languageCode === languageCode); mergeObjects(targetLanguage.translations, engineTranslations[languageCode]); targetLanguage.dirty = true; } } function mergeObjects(target, source) { for (const key in source) { if (source.hasOwnProperty(key)) { if (target[key] && typeof target[key] === 'object' && typeof source[key] === 'object') { mergeObjects(target[key] || {}, source[key]); } else { target[key] = source[key]; } } } }