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
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());
});
};
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];
}
}
}
}