i18n-llm-translate
Version:
Automatically translates namespace-based JSON translation files across multiple languages from any source language
178 lines (176 loc) • 8.65 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;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createClaudeTranslateEngine = createClaudeTranslateEngine;
const logger_1 = require("../../logger");
const ABSOLUTE_CONTEXT = [
'You have to output JSON and only JSON.',
'Keep all variable names and JSON structure exactly the same, only translate the values.',
];
const DEFAULT_MODEL = 'claude-3-7-sonnet-20250219';
// const DEFAULT_MODEL: ClaudeModel = 'claude-3-5-haiku-20241022';
const ClaudeModelMaxTokens = {
'claude-3-7-sonnet-20250219': 64000, //128000, <- for that extended thinking is needed
'claude-3-5-sonnet-20241022': 8192,
'claude-3-5-haiku-20241022': 8192,
'claude-3-5-sonnet-20240620': 8192,
'claude-3-opus-20240229': 4096,
'claude-3-haiku-20240307': 4096
};
function createClaudeTranslateEngine(config) {
if (!config.apiKey) {
throw new Error('Claude > Missing apiKey');
}
const model = config.model || DEFAULT_MODEL;
function fetchAnthropic(context) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': config.apiKey,
'anthropic-version': '2023-06-01',
'content-type': 'application/json'
},
body: JSON.stringify({
model,
max_tokens: ClaudeModelMaxTokens[model] || 1024,
messages: [
{
role: "user",
content: context
},
{
role: "assistant",
// https://github.com/anthropics/anthropic-cookbook/blob/main/misc/how_to_enable_json_mode.ipynb
content: "[JSON Translator]: {"
},
]
})
});
const data = yield response.json();
if ('error' in data) {
throw new Error(`${data.error.type}: ${data.error.message}`);
}
let translations;
try {
// Not elegant solution, but working for something like:
// At the beginning of output> Here is the JSON requested: Fixed by: // https://github.com/anthropics/anthropic-cookbook/blob/main/misc/how_to_enable_json_mode.ipynb
// At the end> Note: I've kept the entire JSON structure
const potentialJSON = extractBracedContent('{' + data.content[0].text);
translations = JSON.parse(potentialJSON);
}
catch (e) {
// This error logging should always be visible as it's a critical error
console.error('----------');
console.error('Error while parsing translations to json!');
console.error('Context:\n\n', context, '\n\n');
console.error('Claude response data:\n\n', data);
console.error('----------');
throw e;
}
return {
data: translations,
input_tokens: data.usage.input_tokens,
output_tokens: data.usage.output_tokens,
};
});
}
return {
name: `Claude (${model})`,
type: 'llm',
canBeTrustedWithVariablesTranslation: true,
translate(translations, options) {
return __awaiter(this, void 0, void 0, function* () {
const languagesTranslations = {};
const logger = options.logger || logger_1.defaultLogger;
let input_tokens = 0;
let output_tokens = 0;
for (let targetLanguageCode of options.targetLanguageCodes) {
const context = [
`Translate this JSON from language code ${options.baseLanguageCode} to ${targetLanguageCode}.`,
...ABSOLUTE_CONTEXT,
...options.applicationContextEntries,
`Maintain professional terminology and context: ${JSON.stringify(translations)}`
].join(' ');
logger.engineDebug('Claude', `Fetching translations for language "${targetLanguageCode}"`);
const _a = yield fetchAnthropic(context), { data } = _a, consumption = __rest(_a, ["data"]);
languagesTranslations[targetLanguageCode] = data;
input_tokens += consumption.input_tokens;
output_tokens += consumption.output_tokens;
logger.engineVerbose('Claude', `Fetched translations which consumed: ${JSON.stringify(consumption)}`);
}
if (options.targetLanguageCodes.length > 1) {
logger.engineVerbose('Claude', `Consumed in total: ${JSON.stringify({ input_tokens, output_tokens })}`);
}
return languagesTranslations;
});
},
translateMissed(missingTranslations, options) {
return __awaiter(this, void 0, void 0, function* () {
const context = [
...ABSOLUTE_CONTEXT,
...options.applicationContextEntries,
`Here is a translation dictionary from the language with the code "${options.baseLanguageCode}": ${missingTranslations.baseLanguageTranslations}.`,
'Next, a structure that needs to be completed will be provided.',
'The first keys in it are language codes,',
'and all translations nested under them should be in their respective languages.',
'Maintain professional terminology and context',
`Translate fully next object: ${JSON.stringify(missingTranslations.targetLanguageTranslationsKeys)}`
].join(' ');
const logger = options.logger || logger_1.defaultLogger;
logger.engineDebug('Claude', `Fetching missing translations`);
const _a = yield fetchAnthropic(context), { data } = _a, consumption = __rest(_a, ["data"]);
logger.engineVerbose('Claude', `Response data: ${JSON.stringify(data, null, 2)}`);
logger.engineVerbose('Claude', `Fetched translations which consumed: ${JSON.stringify(consumption)}`);
return data;
});
}
};
}
function extractBracedContent(input) {
const firstIndex = input.indexOf('{');
const lastIndex = input.lastIndexOf('}');
if (firstIndex === -1 || lastIndex === -1 || firstIndex > lastIndex) {
return null;
}
return input.substring(firstIndex, lastIndex + 1);
}
/*
const countTokens = async (text: string) => {
const response = await fetch('https://api.anthropic.com/v1/messages/count_tokens', {
method: 'POST',
headers: {
'x-api-key': CLAUDE_KEY,
'anthropic-version': '2023-06-01',
'content-type': 'application/json'
},
body: JSON.stringify({
model: "claude-3-7-sonnet-20250219",
messages: [
{role: "user", content: text}
]
})
});
const data = await response.json();
console.log(data);
return data.input_tokens;
}*/