UNPKG

i18n-llm-translate

Version:

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

300 lines (299 loc) 15.2 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.stripEmptyStringLeavesFromDiff = stripEmptyStringLeavesFromDiff; exports.readTranslationsNamespaces = readTranslationsNamespaces; exports.applyEngineTranslations = applyEngineTranslations; const util_1 = require("./util"); const logger_1 = require("./logger"); 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.`); } } } // 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 getLeafAtPath(root, path) { let cur = root; for (const k of path) { if (cur == null || typeof cur !== 'object') return undefined; cur = cur[k]; } return cur; } function setLeafAtPath(root, path, value) { let cur = root; for (let i = 0; i < path.length - 1; i++) { const k = path[i]; if (!cur[k] || typeof cur[k] !== 'object') cur[k] = {}; cur = cur[k]; } cur[path[path.length - 1]] = value; } /** Removes trim-empty string leaves from a base-language diff; copies those values to all targets (no API). */ function stripEmptyStringLeavesFromDiff(diff, namespace, path = []) { const result = {}; for (const key of Object.keys(diff)) { const val = diff[key]; const nextPath = [...path, key]; if (typeof val === 'object' && val !== null && !Array.isArray(val)) { const inner = stripEmptyStringLeavesFromDiff(val, namespace, nextPath); if (inner && Object.keys(inner).length > 0) { result[key] = inner; } } else if (typeof val === 'string' && val.trim() === '') { for (const tl of namespace.targetLanguages) { if (getLeafAtPath(tl.translations, nextPath) !== val) { setLeafAtPath(tl.translations, nextPath, val); tl.dirty = true; } } } else { result[key] = val; } } return Object.keys(result).length > 0 ? result : undefined; } function readTranslationsNamespaces(options) { return __awaiter(this, void 0, void 0, function* () { const logger = options.logger || logger_1.defaultLogger; 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 (_a) { // 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) { logger.error(`Error writing to ${targetFilePath}:`, error); } } if (savedLanguages.length) { const languages = savedLanguages.length === 1 ? savedLanguages[0] : `(${savedLanguages.join(', ')})`; logger.verbose(`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"]); const sub = translations[key]; targetDig.push(Object.assign({ translations: typeof sub === 'object' && sub !== null && !Array.isArray(sub) ? sub : {} }, 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; } if (typeof value === 'string' && value.trim() === '') { for (let languageContainer of targetLanguagesTranslations) { if (languageContainer.translations[key] !== value) { languageContainer.translations[key] = value; languageContainer.dirty = true; } } continue; } let foundMissed = false; for (let languageContainer of targetLanguagesTranslations) { if (!(key in languageContainer.translations)) { 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) { logger.error(`Error reading "${baseLanguageDirectory}/${fileName}":`, error); throw error; } } for (let namespace of namespaces) { try { validateTranslationStructure(namespace.baseLanguageTranslations); } catch (error) { logger.error(`Translation namespace "${baseLanguageDirectory}/${namespace.jsonFileName}" not valid:`, error); 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 (_b) { logger.verbose(`Creating directory for target language: ${targetLanguageCode}`); yield fs_1.promises.mkdir(targetLanguageDirectory, { recursive: true }); } const missingFiles = []; 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 (_c) { missingFiles.push(namespace.jsonFileName); } try { content = JSON.parse(contentString); } catch (error) { logger.warn(`"${targetLanguageCode}/${namespace.jsonFileName}" invalid JSON.`); // noinspection ExceptionCaughtLocallyJS throw error; } namespace.targetLanguages.push({ dirty: false, languageCode: targetLanguageCode, translations: cleanTargetTranslations(namespace.baseLanguageTranslations, content), }); } catch (error) { logger.error(`Error reading "${targetLanguageCode}/${namespace.jsonFileName}":`, error); throw error; } } if (missingFiles.length > 0) { logger.verbose(`Language code "${targetLanguageCode}" missing files: ${missingFiles.map(n => `"${n}"`).join(', ')}. Initializing empty JSON.`); } } 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]; } } } }