i18n-ai-translate
Version:
AI-powered localization CLI, Node library, and GitHub Action. Translate i18next JSON, Gettext PO, Java .properties, and iOS .strings with ChatGPT, Claude, Gemini, or local Ollama models.
987 lines (857 loc) • 35.7 kB
text/typescript
import { RETRY_ATTEMPTS } from "../constants";
import { Tiktoken, getEncoding } from "js-tiktoken";
import {
TranslateItemOutputObjectSchema,
VerifyItemOutputObjectSchema,
} from "./types";
import {
getMissingVariables,
getTemplatedStringRegex,
printError,
printExecutionTime,
printProgress,
printWarn,
} from "../utils";
import { retryWithBackoff } from "../retry";
import { runAcrossShards } from "../shard_runner";
import { translationPromptJSON, verificationPromptJSON } from "./prompts";
import type {
GenerateStateJSON,
TranslateItem,
TranslateItemInput,
TranslateItemOutput,
VerifyItemInput,
VerifyItemOutput,
} from "./types";
import type { TranslationStatsItem } from "../types";
import type { ZodType, ZodTypeDef } from "zod";
import type Chats from "../interfaces/chats";
import type GenerateTranslationOptionsJSON from "../interfaces/generate_translation_options_json";
import type RateLimiter from "../rate_limiter";
import type TranslateOptions from "../interfaces/translate_options";
import type TranslationContext from "../interfaces/translation_context";
export default class GenerateTranslationJSON {
tikToken: Tiktoken;
templatedStringRegex: RegExp;
constructor(options: TranslateOptions) {
// js-tiktoken is a pure-JS port — no WASM required, so global
// installs work on Windows without the tiktoken_bg.wasm dance
// that plagued the native tiktoken package (issue #428).
this.tikToken = getEncoding("cl100k_base");
this.templatedStringRegex = getTemplatedStringRegex(
options.templatedStringPrefix as string,
options.templatedStringSuffix as string,
);
}
/**
* Complete the initial translation of the input text.
* @param flatInput - The flatinput object containing the json to translate
* @param options - The options to generate the translation
* @param chats - The options to generate the translation
* @param translationStats - The translation statistics
*/
public async translateJSON(
ctx: TranslationContext,
): Promise<{ [key: string]: string }> {
const { flatInput, options, pool, groups, stats } = ctx;
// Seed stats once up front; per-shard work then just increments
// the shared counters.
const allItems = this.generateTranslateItemArray(flatInput);
stats.translate.totalItems = allItems.length;
stats.translate.totalTokens = allItems.reduce(
(sum, item) => sum + item.translationTokens,
0,
);
stats.translate.batchStartTime = Date.now();
const perShardResults = await runAcrossShards(
flatInput,
groups,
pool,
async (shard, chats) => {
const shardItems = this.generateTranslateItemArray(shard);
const translated = await this.generateTranslationJSON(
shardItems,
options,
chats,
stats.translate,
pool.rateLimiter,
);
if (options.skipTranslationVerification) {
return translated;
}
return this.generateVerificationJSON(
translated,
options,
chats,
stats.verify,
pool.rateLimiter,
);
},
);
const combined: TranslateItem[] = [];
for (const shardResult of perShardResults) {
combined.push(...shardResult);
}
return this.convertTranslateItemToIndex(combined);
}
/**
* Run the verification step against a source/target pair without
* writing anything. Returns one entry per invalid key, reporting
* what the model thought was wrong and what it would have fixed
* the translation to.
*
* This deliberately does NOT go through generateVerificationJSON —
* that path is designed to *fix* failures and re-verify, so
* successfully-fixed items come back with failure="" and the check
* report would miss every issue. Instead we call the verification
* prompt once per batch, parse the raw schema output, and surface
* the `valid: false` items directly.
*/
public async checkJSON(ctx: {
flatSource: { [key: string]: string };
flatTarget: { [key: string]: string };
options: TranslateOptions;
pool: import("../chat_pool").default;
}): Promise<
Array<{
key: string;
original: string;
translated: string;
issue: string;
suggestion: string;
}>
> {
const { flatSource, flatTarget, options, pool } = ctx;
// Build items pre-populated with the on-disk translation — the
// verify prompt expects both `original` and `translated` to be
// filled in before it runs.
const items: TranslateItem[] = [];
let id = 1;
for (const key in flatSource) {
if (
!Object.prototype.hasOwnProperty.call(flatSource, key) ||
!(key in flatTarget)
) {
continue;
}
const item = this.generateTranslateItem(id, key, flatSource[key]);
item.translated = flatTarget[key];
item.verificationTokens = this.getVerifyItemToken(item);
items.push(item);
id++;
}
if (items.length === 0) return [];
const [chats] = pool.all();
const issues: Array<{
key: string;
original: string;
translated: string;
issue: string;
suggestion: string;
}> = [];
// Batch the items to stay within batchSize / batchMaxTokens.
// getBatchVerifyItemArray already handles token-aware slicing.
let remaining = items.slice();
while (remaining.length > 0) {
const batch = this.getBatchVerifyItemArray(remaining, options);
if (batch.length === 0) break;
remaining = remaining.filter((it) => !batch.includes(it));
const promptText = verificationPromptJSON(
options.inputLanguageCode,
options.outputLanguageCode,
this.generateVerifyItemsInput(batch),
{
context: options.context,
glossary: options.glossary,
overridePrompt: options.overridePrompt,
templatedStringPrefix: options.templatedStringPrefix,
templatedStringSuffix: options.templatedStringSuffix,
},
);
// eslint-disable-next-line no-await-in-loop
const raw = await chats.verifyTranslationChat.sendMessage(
promptText,
VerifyItemOutputObjectSchema,
);
const parsed = this.parseVerificationToJSON(raw);
const idToItem = new Map(batch.map((i) => [i.id, i]));
for (const v of parsed) {
if (!this.isValidVerificationItem(v)) continue;
if (v.valid) continue;
const item = idToItem.get(v.id);
if (!item) continue;
issues.push({
issue: v.issue || "Flagged by verifier",
key: item.key,
original: item.original,
suggestion: v.fixedTranslation ?? "",
translated: flatTarget[item.key] ?? "",
});
}
}
return issues;
}
private generateTranslateItemsInput(
translateItems: TranslateItem[],
): TranslateItemInput[] {
return translateItems.map(
(translateItem) =>
({
// Only adds 'context' to the object if it's not empty. Makes the prompt shorter and uses less tokens
...(translateItem.context !== ""
? { context: translateItem.context }
: {}),
...(translateItem.failure !== ""
? { failure: translateItem.failure }
: {}),
id: translateItem.id,
original: translateItem.original,
}) as TranslateItemInput,
);
}
private generateVerifyItemsInput(
verifyItems: TranslateItem[],
): VerifyItemInput[] {
return verifyItems.map(
(verifyItem) =>
({
...(verifyItem.context !== ""
? { context: verifyItem.context }
: {}),
...(verifyItem.failure !== ""
? { failure: verifyItem.failure }
: {}),
id: verifyItem.id,
original: verifyItem.original,
translated: verifyItem.translated,
}) as VerifyItemInput,
);
}
private generateTranslateItem(
id: number,
key: string,
original: string,
): TranslateItem {
const translateItem = {
context: "",
failure: "",
id,
key,
original,
templateStrings: [],
translated: "",
translationAttempts: 0,
translationTokens: 0,
verificationAttempts: 0,
verificationTokens: 0,
} as TranslateItem;
// Maps the 'placeholders' in the translated object to make sure that none are missing
const match = original.match(this.templatedStringRegex);
if (match) {
translateItem.templateStrings = match;
}
// Tokens here are used to estimate accurately the execution time
translateItem.translationTokens =
this.getTranslateItemToken(translateItem);
return translateItem;
}
private getBatchTranslateItemArray(
translateItemArray: TranslateItem[],
options: TranslateOptions,
): TranslateItem[] {
const promptTokens = this.tikToken.encode(
translationPromptJSON(
options.inputLanguageCode,
options.outputLanguageCode,
[],
{
context: options.context,
glossary: options.glossary,
overridePrompt: options.overridePrompt,
templatedStringPrefix: options.templatedStringPrefix,
templatedStringSuffix: options.templatedStringSuffix,
},
),
).length;
// Remove the tokens used by the prompt and divide the remaining tokens divided by 2 (half for the input/output) with a 10% margin of error
const maxInputTokens =
((Number(options.batchMaxTokens) - promptTokens) * 0.9) / 2;
let currentTokens = 0;
const batchTranslateItemArray: TranslateItem[] = [];
for (const translateItem of translateItemArray) {
// If a failure message is added the tokens for an item change
currentTokens +=
translateItem.failure !== ""
? this.getTranslateItemToken(translateItem)
: translateItem.translationTokens;
if (
batchTranslateItemArray.length !== 0 &&
(currentTokens >= maxInputTokens ||
batchTranslateItemArray.length >= Number(options.batchSize))
) {
break;
}
batchTranslateItemArray.push(translateItem);
if (translateItem.translationAttempts > 5) {
// Add a minimum of one items if the item has been tried many times
// Too many items can cause translations to fail
break;
}
}
return batchTranslateItemArray;
}
private getBatchVerifyItemArray(
translatedItemArray: TranslateItem[],
options: TranslateOptions,
): TranslateItem[] {
const promptTokens = this.tikToken.encode(
verificationPromptJSON(
options.inputLanguageCode,
options.outputLanguageCode,
[],
{
context: options.context,
glossary: options.glossary,
overridePrompt: options.overridePrompt,
templatedStringPrefix: options.templatedStringPrefix,
templatedStringSuffix: options.templatedStringSuffix,
},
),
).length;
const maxInputTokens =
((Number(options.batchMaxTokens) - promptTokens) * 0.9) / 2;
let currentTokens = 0;
const batchVerifyItemArray: TranslateItem[] = [];
for (const translatedItem of translatedItemArray) {
currentTokens +=
translatedItem.failure !== ""
? this.getVerifyItemToken(translatedItem)
: translatedItem.verificationTokens;
if (
batchVerifyItemArray.length !== 0 &&
(currentTokens >= maxInputTokens ||
batchVerifyItemArray.length >= Number(options.batchSize))
) {
break;
}
batchVerifyItemArray.push(translatedItem);
if (translatedItem.verificationAttempts > 5) {
// Add a minimum of one items if the item has been tried many times
// Too many items can cause translations to fail
break;
}
}
return batchVerifyItemArray;
}
private generateTranslateItemArray(flatInput: any): TranslateItem[] {
return Object.keys(flatInput).reduce((acc, key) => {
if (Object.prototype.hasOwnProperty.call(flatInput, key)) {
acc.push(
this.generateTranslateItem(
Object.keys(flatInput).indexOf(key) + 1,
key,
flatInput[key],
),
);
}
return acc;
}, [] as TranslateItem[]);
}
private getTranslateItemToken(translatedItem: TranslateItem): number {
return this.tikToken.encode(
JSON.stringify(
this.generateTranslateItemsInput([translatedItem])[0],
),
).length;
}
private getVerifyItemToken(translatedItem: TranslateItem): number {
return this.tikToken.encode(
JSON.stringify(this.generateVerifyItemsInput([translatedItem])[0]),
).length;
}
private async generateTranslationJSON(
translateItemArray: TranslateItem[],
options: TranslateOptions,
chats: Chats,
translationStats: TranslationStatsItem,
rateLimiter?: RateLimiter,
): Promise<TranslateItem[]> {
const generatedTranslation: TranslateItem[] = [];
// totalItems / totalTokens / batchStartTime are set once by the
// public translateJSON entry point so parallel shards don't
// clobber one another's stats.
const skippedItems: TranslateItem[] = [];
// translate items are removed from 'translateItemArray' when one is generated
// this is done to avoid 'losing' items if the model doesn't return one
while (translateItemArray.length > 0) {
const batchTranslateItemArray = this.getBatchTranslateItemArray(
translateItemArray,
options,
);
for (const batchTranslateItem of batchTranslateItemArray) {
batchTranslateItem.translationAttempts++;
if (batchTranslateItem.translationAttempts > RETRY_ATTEMPTS) {
if (options.continueOnError) {
printError(
`Skipping key after ${RETRY_ATTEMPTS} failed translation attempts: ${batchTranslateItem.key}`,
);
const idx = translateItemArray.findIndex(
(item) => item.id === batchTranslateItem.id,
);
if (idx !== -1) translateItemArray.splice(idx, 1);
skippedItems.push(batchTranslateItem);
continue;
}
return Promise.reject(
new Error(
`Item failed to translate too many times: ${JSON.stringify(batchTranslateItem)}. If this persists try a different model`,
),
);
}
}
const filteredBatch = batchTranslateItemArray.filter(
(item) => item.translationAttempts <= RETRY_ATTEMPTS,
);
if (filteredBatch.length === 0) continue;
translationStats.enqueuedItems += filteredBatch.length;
// eslint-disable-next-line no-await-in-loop
const result = await this.runTranslationJob({
chats,
context: options.context,
ensureChangedTranslation:
options.ensureChangedTranslation as boolean,
glossary: options.glossary,
inputLanguageCode: options.inputLanguageCode,
outputLanguageCode: options.outputLanguageCode,
overridePrompt: options.overridePrompt,
rateLimiter,
skipStylingVerification:
options.skipStylingVerification as boolean,
skipTranslationVerification:
options.skipTranslationVerification as boolean,
templatedStringPrefix: options.templatedStringPrefix as string,
templatedStringSuffix: options.templatedStringSuffix as string,
translateItems: filteredBatch,
verboseLogging: options.verbose as boolean,
});
if (!result) {
return Promise.reject(new Error("Translation job failed"));
}
for (const translatedItem of result) {
// Check if the translated item exists in the untranslated item array
const index = translateItemArray.findIndex(
(item) => item.id === translatedItem.id,
);
if (index !== -1) {
// If it does remove it from the 'translateItemArray' used to queue items for translation
translateItemArray.splice(index, 1);
// Prepare the object then add it to results
translatedItem.verificationTokens =
this.getVerifyItemToken(translatedItem);
generatedTranslation.push(translatedItem);
translationStats.processedTokens +=
translatedItem.translationTokens;
}
translationStats.processedItems++;
}
printProgress(
options.skipTranslationVerification
? "Translating"
: "Step 1/2 - Translating",
translationStats.batchStartTime,
translationStats.totalTokens,
translationStats.processedTokens,
);
}
printExecutionTime(
translationStats.batchStartTime,
"\nTranslation execution time: ",
);
if (skippedItems.length > 0) {
printError(
`Skipped ${skippedItems.length} key(s) that exhausted retries: ${skippedItems
.map((item) => item.key)
.join(", ")}`,
);
}
return generatedTranslation;
}
private async generateVerificationJSON(
verifyItemArray: TranslateItem[],
options: TranslateOptions,
chats: Chats,
translationStats: TranslationStatsItem,
rateLimiter?: RateLimiter,
): Promise<TranslateItem[]> {
const generatedVerification: TranslateItem[] = [];
// Stats counters are set centrally in translateJSON so concurrent
// shards don't clobber each other.
if (translationStats.batchStartTime === 0) {
translationStats.batchStartTime = Date.now();
}
translationStats.totalItems += verifyItemArray.length;
translationStats.totalTokens += verifyItemArray.reduce(
(sum, verifyItem) => sum + verifyItem.verificationTokens,
0,
);
while (verifyItemArray.length > 0) {
const batchVerifyItemArray = this.getBatchVerifyItemArray(
verifyItemArray,
options,
);
for (const batchVerifyItem of batchVerifyItemArray) {
batchVerifyItem.verificationAttempts++;
if (batchVerifyItem.verificationAttempts > RETRY_ATTEMPTS) {
if (options.continueOnError) {
printError(
`Skipping key after ${RETRY_ATTEMPTS} failed verification attempts; accepting unverified translation: ${batchVerifyItem.key}`,
);
const idx = verifyItemArray.findIndex(
(item) => item.id === batchVerifyItem.id,
);
if (idx !== -1) verifyItemArray.splice(idx, 1);
generatedVerification.push(batchVerifyItem);
continue;
}
return Promise.reject(
new Error(
`Item failed to verify too many times: ${JSON.stringify(batchVerifyItem)}. If this persists try a different model`,
),
);
}
}
const filteredVerifyBatch = batchVerifyItemArray.filter(
(item) => item.verificationAttempts <= RETRY_ATTEMPTS,
);
if (filteredVerifyBatch.length === 0) continue;
translationStats.enqueuedItems += filteredVerifyBatch.length;
// eslint-disable-next-line no-await-in-loop
const result = await this.runVerificationJob({
chats,
context: options.context,
ensureChangedTranslation:
options.ensureChangedTranslation as boolean,
glossary: options.glossary,
inputLanguageCode: options.inputLanguageCode,
outputLanguageCode: options.outputLanguageCode,
overridePrompt: options.overridePrompt,
rateLimiter,
skipStylingVerification:
options.skipStylingVerification as boolean,
skipTranslationVerification:
options.skipTranslationVerification as boolean,
templatedStringPrefix: options.templatedStringPrefix as string,
templatedStringSuffix: options.templatedStringSuffix as string,
translateItems: filteredVerifyBatch,
verboseLogging: options.verbose as boolean,
});
if (!result) {
return Promise.reject(new Error("Verification job failed"));
}
for (const translatedItem of result) {
const index = verifyItemArray.findIndex(
(item) => item.id === translatedItem.id,
);
if (index !== -1) {
verifyItemArray.splice(index, 1);
generatedVerification.push(translatedItem);
translationStats.processedTokens +=
translatedItem.verificationTokens;
}
translationStats.processedItems++;
}
printProgress(
"Step 2/2 - Verifying",
translationStats.batchStartTime,
translationStats.totalTokens,
translationStats.processedTokens,
);
}
printExecutionTime(
translationStats.batchStartTime,
"Verification execution time: ",
);
return generatedVerification;
}
private convertTranslateItemToIndex(
generatedTranslation: TranslateItem[],
): {
[key: string]: string;
} {
return generatedTranslation.reduce(
(acc, translation) => {
acc[translation.key] = translation.translated;
return acc;
},
{} as { [key: string]: string },
);
}
private parseTranslationToJSON(outputText: string): TranslateItemOutput[] {
try {
return TranslateItemOutputObjectSchema.parse(JSON.parse(outputText))
.items;
} catch (error) {
printError(
`Error parsing JSON: '${error}', output: '${outputText}'\n`,
);
return [];
}
}
private parseVerificationToJSON(outputText: string): VerifyItemOutput[] {
try {
return VerifyItemOutputObjectSchema.parse(JSON.parse(outputText))
.items;
} catch (error) {
printError(
`Error parsing JSON: '${error}', output: '${outputText}'\n`,
);
return [];
}
}
private isValidTranslateItem(
item: TranslateItemOutput,
): item is TranslateItemOutput {
return (
typeof item.id === "number" &&
typeof item.translated === "string" &&
item.id > 0
);
}
private isValidVerificationItem(
item: VerifyItemOutput,
): item is VerifyItemOutput {
if (!(typeof item.id === "number")) return false;
if (!(typeof item.valid === "boolean")) return false;
if (item.id <= 0) return false;
// 'fixedTranslation' should be a translation if valid is false
if (
item.valid === false &&
!(typeof item.fixedTranslation === "string")
)
return false;
return true;
}
private createTranslateItemsWithTranslation(
untranslatedItems: TranslateItem[],
translatedItems: TranslateItemOutput[],
): TranslateItem[] {
const output: TranslateItem[] = [];
for (const untranslatedItem of untranslatedItems) {
const translatedItem = translatedItems.find(
(checkTranslatedItem) =>
untranslatedItem.id === checkTranslatedItem.id,
);
if (translatedItem) {
untranslatedItem.translated = translatedItem.translated;
if (translatedItem.translated === "") {
untranslatedItem.failure =
"The translated value cannot be an empty string";
continue;
}
const templateStrings =
translatedItem.translated.match(
this.templatedStringRegex,
) ?? [];
const missingVariables = getMissingVariables(
untranslatedItem.templateStrings,
templateStrings,
);
if (missingVariables.length !== 0) {
// Item is updated with a failure message. This message gives the LLM a context to help it fix the translation.
// Without this the same error is made over and over again, with the message the new translation is generally accepted.
untranslatedItem.failure = `Ensure all variables are included. The following variables are missing from the previous translation and must be added: '${JSON.stringify(missingVariables)}'`;
continue;
}
output.push({
...untranslatedItem,
failure: "",
} as TranslateItem);
}
}
return output;
}
private createVerifyItemsWithTranslation(
translatedItemArray: TranslateItem[],
verifiedItemArray: VerifyItemOutput[],
): TranslateItem[] {
const output: TranslateItem[] = [];
for (const translatedItem of translatedItemArray) {
const verifiedItem = verifiedItemArray.find(
(checkVerifiedItem) =>
translatedItem.id === checkVerifiedItem.id,
);
if (verifiedItem) {
if (verifiedItem.valid) {
output.push({
...translatedItem,
failure: "",
} as TranslateItem);
} else {
translatedItem.translated =
verifiedItem.fixedTranslation as string;
if (verifiedItem.fixedTranslation === "") {
translatedItem.failure =
"The translated value cannot be an empty string";
continue;
}
const templateStrings =
verifiedItem.fixedTranslation.match(
this.templatedStringRegex,
) ?? [];
const missingVariables = getMissingVariables(
translatedItem.templateStrings,
templateStrings,
);
if (missingVariables.length !== 0) {
translatedItem.failure = `Must add variables, missing from last translation: '${JSON.stringify(missingVariables)}'`;
continue;
}
// 'translatedItem' is updated and queued again to check if the new fixed translation is valid
translatedItem.failure = `Previous issue that should be corrected: '${verifiedItem.issue}'`;
}
}
}
return output;
}
private async runTranslationJob(
options: GenerateTranslationOptionsJSON,
): Promise<TranslateItem[]> {
const generateState: GenerateStateJSON = {
fixedTranslationMappings: {},
generationRetries: 0,
translationToRetryAttempts: {},
};
const generationPromptText = translationPromptJSON(
options.inputLanguageCode,
options.outputLanguageCode,
this.generateTranslateItemsInput(options.translateItems),
{
context: options.context,
glossary: options.glossary,
keys: options.translateItems.map((it) => it.key),
overridePrompt: options.overridePrompt,
templatedStringPrefix: options.templatedStringPrefix,
templatedStringSuffix: options.templatedStringSuffix,
},
);
let translated = "";
try {
translated = await retryWithBackoff(
() =>
this.generateJob(
generationPromptText,
options,
generateState,
TranslateItemOutputObjectSchema,
),
{
maxRetries: RETRY_ATTEMPTS,
rateLimiter: options.rateLimiter,
verbose: options.verboseLogging,
},
);
} catch (e) {
printError(`Failed to translate: ${e}\n`);
}
const parsedOutput = this.parseTranslationToJSON(translated);
const validTranslationObjects = parsedOutput.filter(
this.isValidTranslateItem,
);
return this.createTranslateItemsWithTranslation(
options.translateItems,
validTranslationObjects,
);
}
private async runVerificationJob(
options: GenerateTranslationOptionsJSON,
): Promise<TranslateItem[]> {
const generateState: GenerateStateJSON = {
fixedTranslationMappings: {},
generationRetries: 0,
translationToRetryAttempts: {},
};
const generationPromptText = verificationPromptJSON(
options.inputLanguageCode,
options.outputLanguageCode,
this.generateVerifyItemsInput(options.translateItems),
{
context: options.context,
glossary: options.glossary,
overridePrompt: options.overridePrompt,
templatedStringPrefix: options.templatedStringPrefix,
templatedStringSuffix: options.templatedStringSuffix,
},
);
let verified = "";
try {
verified = await retryWithBackoff(
() =>
this.generateJob(
generationPromptText,
options,
generateState,
VerifyItemOutputObjectSchema,
),
{
maxRetries: RETRY_ATTEMPTS,
rateLimiter: options.rateLimiter,
verbose: options.verboseLogging,
},
);
} catch (e) {
printError(`Failed to translate: ${e}\n`);
}
const parsedOutput = this.parseVerificationToJSON(verified);
const validTranslationObjects = parsedOutput.filter(
this.isValidVerificationItem,
);
return this.createVerifyItemsWithTranslation(
options.translateItems,
validTranslationObjects,
);
}
private verifyGenerationAndRetry(
generationPromptText: string,
options: GenerateTranslationOptionsJSON,
generateState: GenerateStateJSON,
): Promise<string> {
generateState.generationRetries++;
if (generateState.generationRetries > 10) {
options.chats.generateTranslationChat.resetChatHistory();
return Promise.reject(
new Error(
"Failed to generate content due to exception. Resetting history.",
),
);
}
printError(`Erroring text = ${generationPromptText}\n`);
options.chats.generateTranslationChat.rollbackLastMessage();
return Promise.reject(
new Error("Failed to generate content due to exception."),
);
}
private async generateJob(
generationPromptText: string,
options: GenerateTranslationOptionsJSON,
generateState: GenerateStateJSON,
format: ZodType<any, ZodTypeDef, any>,
): Promise<string> {
const text = await options.chats.generateTranslationChat.sendMessage(
generationPromptText,
format,
);
if (!text) {
return this.verifyGenerationAndRetry(
generationPromptText,
options,
generateState,
);
} else {
generateState.generationRetries = 0;
}
if (options.verboseLogging) {
printWarn(text);
}
return text;
}
}