@adminforth/i18n 1.10.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.log +3 -2
- package/custom/BulkActionButton.vue +59 -15
- package/custom/TranslationJobViewComponent.vue +82 -0
- package/dist/custom/BulkActionButton.vue +59 -15
- package/dist/custom/TranslationJobViewComponent.vue +82 -0
- package/dist/index.js +279 -93
- package/index.ts +382 -118
- package/package.json +3 -2
- package/types.ts +14 -0
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes, RAMLock } from "adminforth";
|
|
10
|
+
import { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes, RAMLock, filtersTools, AdminForthFilterOperators } from "adminforth";
|
|
11
11
|
import iso6391 from 'iso-639-1';
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import fs from 'fs-extra';
|
|
@@ -15,6 +15,8 @@ import chokidar from 'chokidar';
|
|
|
15
15
|
import { AsyncQueue } from '@sapphire/async-queue';
|
|
16
16
|
import getFlagEmoji from 'country-flag-svg';
|
|
17
17
|
import { parse } from 'bcp-47';
|
|
18
|
+
import pLimit from 'p-limit';
|
|
19
|
+
import { afLogger } from "adminforth";
|
|
18
20
|
const processFrontendMessagesQueue = new AsyncQueue();
|
|
19
21
|
const SLAVIC_PLURAL_EXAMPLES = {
|
|
20
22
|
uk: 'яблук | Яблуко | Яблука | Яблук', // zero | singular | 2-4 | 5+
|
|
@@ -146,6 +148,12 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
146
148
|
return __awaiter(this, void 0, void 0, function* () {
|
|
147
149
|
var _a, _b;
|
|
148
150
|
_super.modifyResourceConfig.call(this, adminforth, resourceConfig);
|
|
151
|
+
if (!this.adminforth.config.componentsToExplicitRegister) {
|
|
152
|
+
this.adminforth.config.componentsToExplicitRegister = [];
|
|
153
|
+
}
|
|
154
|
+
this.adminforth.config.componentsToExplicitRegister.push({
|
|
155
|
+
file: this.componentPath('TranslationJobViewComponent.vue')
|
|
156
|
+
});
|
|
149
157
|
// validate each supported language: ISO 639-1 or BCP-47 with region (e.g., en-GB)
|
|
150
158
|
this.options.supportedLanguages.forEach((lang) => {
|
|
151
159
|
if (!isValidSupportedLanguageTag(lang)) {
|
|
@@ -408,10 +416,10 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
408
416
|
if (!resourceConfig.options.pageInjections.list) {
|
|
409
417
|
resourceConfig.options.pageInjections.list = {};
|
|
410
418
|
}
|
|
411
|
-
if (!resourceConfig.options.pageInjections.list.
|
|
412
|
-
resourceConfig.options.pageInjections.list.
|
|
419
|
+
if (!resourceConfig.options.pageInjections.list.threeDotsDropdownItems) {
|
|
420
|
+
resourceConfig.options.pageInjections.list.threeDotsDropdownItems = [];
|
|
413
421
|
}
|
|
414
|
-
resourceConfig.options.pageInjections.list.
|
|
422
|
+
resourceConfig.options.pageInjections.list.threeDotsDropdownItems.push(pageInjection);
|
|
415
423
|
// if there is menu item with resourceId, add .badge function showing number of untranslated strings
|
|
416
424
|
const addBadgeCountToMenuItem = (menuItem) => {
|
|
417
425
|
this.menuItemWithBadgeId = menuItem.itemId;
|
|
@@ -436,43 +444,9 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
436
444
|
});
|
|
437
445
|
});
|
|
438
446
|
}
|
|
439
|
-
|
|
440
|
-
return __awaiter(this, arguments, void 0, function* (
|
|
441
|
-
|
|
442
|
-
const maxKeysInOneReq = 10;
|
|
443
|
-
if (strings.length === 0) {
|
|
444
|
-
return [];
|
|
445
|
-
}
|
|
446
|
-
const replacedLanguageCodeForTranslations = this.options.translateLangAsBCP47Code && langIsoCode.length === 2 ? this.options.translateLangAsBCP47Code[langIsoCode] : null;
|
|
447
|
-
if (strings.length > maxKeysInOneReq) {
|
|
448
|
-
let totalTranslated = [];
|
|
449
|
-
for (let i = 0; i < strings.length; i += maxKeysInOneReq) {
|
|
450
|
-
const slicedStrings = strings.slice(i, i + maxKeysInOneReq);
|
|
451
|
-
process.env.HEAVY_DEBUG && console.log('🪲🔪slicedStrings len', slicedStrings.length);
|
|
452
|
-
const madeKeys = yield this.translateToLang(langIsoCode, slicedStrings, plurals, translations, updateStrings);
|
|
453
|
-
totalTranslated = totalTranslated.concat(madeKeys);
|
|
454
|
-
}
|
|
455
|
-
return totalTranslated;
|
|
456
|
-
}
|
|
457
|
-
const langCode = replacedLanguageCodeForTranslations ? replacedLanguageCodeForTranslations : langIsoCode;
|
|
458
|
-
const lang = langIsoCode;
|
|
459
|
-
const primaryLang = getPrimaryLanguageCode(lang);
|
|
460
|
-
const langName = iso6391.getName(primaryLang);
|
|
461
|
-
const requestSlavicPlurals = Object.keys(SLAVIC_PLURAL_EXAMPLES).includes(primaryLang) && plurals;
|
|
462
|
-
const region = ((_a = String(lang).split('-')[1]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || '';
|
|
463
|
-
const prompt = `
|
|
464
|
-
I need to translate strings in JSON to ${langName} language ${replacedLanguageCodeForTranslations || lang.length > 2 ? `BCP-47 code ${langCode}` : `ISO 639-1 code ${langIsoCode}`} from English for my web app.
|
|
465
|
-
${region ? `Use the regional conventions for ${langCode} (region ${region}), including spelling, punctuation, and formatting.` : ''}
|
|
466
|
-
${requestSlavicPlurals ? `You should provide 4 slavic forms (in format "zero count | singular count | 2-4 | 5+") e.g. "apple | apples" should become "${SLAVIC_PLURAL_EXAMPLES[lang]}"` : ''}
|
|
467
|
-
Keep keys, as is, write translation into values! If keys have variables (in curly brackets), then translated strings should have them as well (variables itself should not be translated). Here are the strings:
|
|
468
|
-
|
|
469
|
-
\`\`\`json
|
|
470
|
-
${JSON.stringify(strings.reduce((acc, s) => {
|
|
471
|
-
acc[s.en_string] = '';
|
|
472
|
-
return acc;
|
|
473
|
-
}, {}), null, 2)}
|
|
474
|
-
\`\`\`
|
|
475
|
-
`;
|
|
447
|
+
generateAndSaveBunch(prompt_1, strings_1, translations_1) {
|
|
448
|
+
return __awaiter(this, arguments, void 0, function* (prompt, strings, translations, updateStrings = {}, lang, failedToTranslate, needToTranslateByLang = {}, jobId, promptCost) {
|
|
449
|
+
// return [];
|
|
476
450
|
const jsonSchemaProperties = {};
|
|
477
451
|
strings.forEach(s => {
|
|
478
452
|
jsonSchemaProperties[s.en_string] = {
|
|
@@ -481,6 +455,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
481
455
|
};
|
|
482
456
|
});
|
|
483
457
|
const jsonSchemaRequired = strings.map(s => s.en_string);
|
|
458
|
+
const dedupRequired = Array.from(new Set(jsonSchemaRequired));
|
|
484
459
|
// call OpenAI
|
|
485
460
|
const resp = yield this.options.completeAdapter.complete(prompt, [], prompt.length * 2, {
|
|
486
461
|
json_schema: {
|
|
@@ -488,7 +463,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
488
463
|
schema: {
|
|
489
464
|
type: "object",
|
|
490
465
|
properties: jsonSchemaProperties,
|
|
491
|
-
required:
|
|
466
|
+
required: dedupRequired,
|
|
492
467
|
},
|
|
493
468
|
},
|
|
494
469
|
});
|
|
@@ -496,32 +471,48 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
496
471
|
if (resp.error) {
|
|
497
472
|
throw new AiTranslateError(resp.error);
|
|
498
473
|
}
|
|
499
|
-
// parse response like
|
|
500
|
-
// Here are the translations for the strings you provided:
|
|
501
|
-
// ```json
|
|
502
|
-
// [{"live": "canlı"}, {"Table Games": "Masa Oyunları"}]
|
|
503
|
-
// ```
|
|
504
474
|
let res;
|
|
505
475
|
try {
|
|
506
|
-
res = resp.content;
|
|
476
|
+
res = resp.content;
|
|
507
477
|
}
|
|
508
478
|
catch (e) {
|
|
509
479
|
console.error(`Error in parsing LLM resp: ${resp}\n Prompt was: ${prompt}\n Resp was: ${JSON.stringify(resp)}`);
|
|
510
|
-
|
|
480
|
+
strings.forEach(s => {
|
|
481
|
+
failedToTranslate.push({
|
|
482
|
+
lang: lang,
|
|
483
|
+
en_string: s.en_string,
|
|
484
|
+
failedReason: "Error in parsing LLM response"
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
return null;
|
|
511
488
|
}
|
|
489
|
+
const backgroundJobsPlugin = this.adminforth.getPluginByClassName('BackgroundJobsPlugin');
|
|
490
|
+
backgroundJobsPlugin.updateJobFieldsAtomically(jobId, () => __awaiter(this, void 0, void 0, function* () {
|
|
491
|
+
// do all set / get fields in this function to make state update atomic and there is no conflicts when 2 tasks in parallel do get before set.
|
|
492
|
+
// don't do long awaits in this callback, since it has exclusive lock.
|
|
493
|
+
let totalUsedTokens = yield backgroundJobsPlugin.getJobField(jobId, 'totalUsedTokens');
|
|
494
|
+
totalUsedTokens += promptCost;
|
|
495
|
+
yield backgroundJobsPlugin.setJobField(jobId, 'totalUsedTokens', totalUsedTokens);
|
|
496
|
+
}));
|
|
512
497
|
try {
|
|
513
498
|
res = JSON.parse(res);
|
|
514
499
|
}
|
|
515
500
|
catch (e) {
|
|
516
501
|
console.error(`Error in parsing LLM resp json: ${resp}\n Prompt was: ${prompt}\n Resp was: ${JSON.stringify(resp)}`);
|
|
517
|
-
|
|
502
|
+
strings.forEach(s => {
|
|
503
|
+
failedToTranslate.push({
|
|
504
|
+
lang: lang,
|
|
505
|
+
en_string: s.en_string,
|
|
506
|
+
failedReason: "Error in parsing LLM response JSON"
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
return null;
|
|
518
510
|
}
|
|
519
511
|
for (const [enStr, translatedStr] of Object.entries(res)) {
|
|
520
512
|
const translationsTargeted = translations.filter(t => t[this.enFieldName] === enStr);
|
|
521
513
|
// might be several with same en_string
|
|
522
514
|
for (const translation of translationsTargeted) {
|
|
523
515
|
//translation[this.trFieldNames[lang]] = translatedStr;
|
|
524
|
-
// process.env.HEAVY_DEBUG && console.log(`🪲translated to ${lang} ${translation.en_string}, ${translatedStr}`)
|
|
525
516
|
if (!updateStrings[translation[this.primaryKeyFieldName]]) {
|
|
526
517
|
updateStrings[translation[this.primaryKeyFieldName]] = {
|
|
527
518
|
updates: {},
|
|
@@ -539,12 +530,134 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
539
530
|
updateStrings[translation[this.primaryKeyFieldName]].updates[this.trFieldNames[lang]] = translatedStr;
|
|
540
531
|
}
|
|
541
532
|
}
|
|
542
|
-
|
|
533
|
+
const langsInvolved = new Set(Object.keys(needToTranslateByLang));
|
|
534
|
+
// here we need to save updateStrings
|
|
535
|
+
yield Promise.all(Object.entries(updateStrings).map((_a) => __awaiter(this, [_a], void 0, function* ([_, { updates, strId }]) {
|
|
536
|
+
// get old full record
|
|
537
|
+
const oldRecord = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.primaryKeyFieldName, strId)]);
|
|
538
|
+
// because this will translate all languages, we can set completedLangs to all languages
|
|
539
|
+
const futureCompletedFieldValue = yield this.computeCompletedFieldValue(Object.assign(Object.assign({}, oldRecord), updates));
|
|
540
|
+
yield this.adminforth.resource(this.resourceConfig.resourceId).update(strId, Object.assign(Object.assign({}, updates), { [this.options.completedFieldName]: futureCompletedFieldValue }));
|
|
541
|
+
})));
|
|
542
|
+
for (const lang of langsInvolved) {
|
|
543
|
+
const categoriesInvolved = new Set();
|
|
544
|
+
for (const { enStr, category } of Object.values(updateStrings)) {
|
|
545
|
+
categoriesInvolved.add(category);
|
|
546
|
+
yield this.cache.clear(`${this.resourceConfig.resourceId}:${category}:${lang}:${enStr}`);
|
|
547
|
+
}
|
|
548
|
+
for (const category of categoriesInvolved) {
|
|
549
|
+
yield this.cache.clear(`${this.resourceConfig.resourceId}:${category}:${lang}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
getTranslateToLangTasks(langIsoCode_1, strings_1) {
|
|
555
|
+
return __awaiter(this, arguments, void 0, function* (langIsoCode, strings, plurals = false, translations, updateStrings = {}, needToTranslateByLang = {}, adminUser) {
|
|
556
|
+
var _a, _b;
|
|
557
|
+
const maxInputTokens = (_a = this.options.inputTokensPerBatch) !== null && _a !== void 0 ? _a : 30000;
|
|
558
|
+
const limit = pLimit(30);
|
|
559
|
+
const enStringsTokenLengthCache = {};
|
|
560
|
+
const tokenLengthPerString = (_a) => __awaiter(this, [_a], void 0, function* ({ str, id }) {
|
|
561
|
+
const objectToPush = {
|
|
562
|
+
en_string: str,
|
|
563
|
+
numOfTokens: yield this.options.completeAdapter.measureTokensCount(`"${str}":"", \n`)
|
|
564
|
+
};
|
|
565
|
+
enStringsTokenLengthCache[id] = objectToPush;
|
|
566
|
+
});
|
|
567
|
+
const promises = strings.map(s => limit(() => tokenLengthPerString({ str: s.en_string, id: s.id })));
|
|
568
|
+
yield Promise.all(promises);
|
|
569
|
+
if (strings.length === 0) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const replacedLanguageCodeForTranslations = this.options.translateLangAsBCP47Code && langIsoCode.length === 2 ? this.options.translateLangAsBCP47Code[langIsoCode] : null;
|
|
573
|
+
const langSchema = parse(String(langIsoCode), {
|
|
574
|
+
normalize: true,
|
|
575
|
+
});
|
|
576
|
+
const langCode = replacedLanguageCodeForTranslations ? replacedLanguageCodeForTranslations : langIsoCode;
|
|
577
|
+
const lang = langIsoCode;
|
|
578
|
+
const primaryLang = getPrimaryLanguageCode(lang);
|
|
579
|
+
const langName = iso6391.getName(primaryLang);
|
|
580
|
+
const requestSlavicPlurals = Object.keys(SLAVIC_PLURAL_EXAMPLES).includes(primaryLang) && plurals;
|
|
581
|
+
const region = langSchema.region;
|
|
582
|
+
const basePrompt = `
|
|
583
|
+
I need to translate strings in JSON to ${langName} language ${replacedLanguageCodeForTranslations || lang.length > 2 ? `BCP-47 code ${langCode}` : `ISO 639-1 code ${langIsoCode}`} from English for my web app.
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
${region ? `Use the regional conventions for ${langCode} (region ${region}), including spelling, punctuation, and formatting.` : ''}
|
|
587
|
+
${requestSlavicPlurals ? `You should provide 4 slavic forms (in format "zero count | singular count | 2-4 | 5+") e.g. "apple | apples" should become "${SLAVIC_PLURAL_EXAMPLES[lang]}"` : ''}
|
|
588
|
+
Keep keys, as is, write translation into values! If keys have variables (in curly brackets), then translated strings should have them as well (variables itself should not be translated). Here are the strings:
|
|
589
|
+
\`\`\`json
|
|
590
|
+
{
|
|
591
|
+
|
|
592
|
+
}
|
|
593
|
+
\`\`\`
|
|
594
|
+
`;
|
|
595
|
+
const failedToTranslate = [];
|
|
596
|
+
const basePromptTokenLength = yield this.options.completeAdapter.measureTokensCount(basePrompt);
|
|
597
|
+
const allowedTokensAmountForFields = maxInputTokens - basePromptTokenLength;
|
|
598
|
+
const stringsToTranslate = Object.fromEntries(strings.map(s => [s.id, s]));
|
|
599
|
+
const generationTasksInitialData = [];
|
|
600
|
+
while (Object.keys(stringsToTranslate).length !== 0) {
|
|
601
|
+
const stringBanch = [];
|
|
602
|
+
const stringIdsInBatch = [];
|
|
603
|
+
let banchTokens = 0;
|
|
604
|
+
for (const id of Object.keys(stringsToTranslate)) {
|
|
605
|
+
const { en_string } = stringsToTranslate[id];
|
|
606
|
+
const numberOfTokensForString = ((_b = enStringsTokenLengthCache[id]) === null || _b === void 0 ? void 0 : _b.numOfTokens) || 0;
|
|
607
|
+
if (banchTokens + numberOfTokensForString <= allowedTokensAmountForFields) {
|
|
608
|
+
stringBanch.push(en_string);
|
|
609
|
+
stringIdsInBatch.push(id);
|
|
610
|
+
banchTokens += numberOfTokensForString;
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (stringBanch.length === 0) {
|
|
617
|
+
Object.values(stringsToTranslate).forEach(s => {
|
|
618
|
+
failedToTranslate.push({
|
|
619
|
+
lang,
|
|
620
|
+
en_string: s.en_string,
|
|
621
|
+
failedReason: "Not enough input generation tokens"
|
|
622
|
+
});
|
|
623
|
+
generationTasksInitialData.push({
|
|
624
|
+
state: {
|
|
625
|
+
failedToTranslate,
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
for (const id of stringIdsInBatch) {
|
|
632
|
+
delete stringsToTranslate[id];
|
|
633
|
+
}
|
|
634
|
+
const promptToGenerate = basePrompt.split(`\`\`\`json`)[0] +
|
|
635
|
+
`\`\`\`json
|
|
636
|
+
{
|
|
637
|
+
${stringBanch.map(s => `"${s}": ""`).join(",\n")}
|
|
638
|
+
}
|
|
639
|
+
\`\`\``;
|
|
640
|
+
const stringBanchCopy = [...stringBanch];
|
|
641
|
+
generationTasksInitialData.push({
|
|
642
|
+
state: {
|
|
643
|
+
taskName: `Translate ${strings.length} strings`,
|
|
644
|
+
prompt: promptToGenerate,
|
|
645
|
+
strings: strings.filter(s => stringBanchCopy.includes(s.en_string)),
|
|
646
|
+
translations: translations.filter(t => stringBanchCopy.includes(t.en_string)),
|
|
647
|
+
updateStrings,
|
|
648
|
+
lang,
|
|
649
|
+
failedToTranslate,
|
|
650
|
+
needToTranslateByLang,
|
|
651
|
+
promptCost: basePromptTokenLength + banchTokens,
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
return generationTasksInitialData;
|
|
543
656
|
});
|
|
544
657
|
}
|
|
545
658
|
// returns translated count
|
|
546
659
|
bulkTranslate(_a) {
|
|
547
|
-
return __awaiter(this, arguments, void 0, function* ({ selectedIds, selectedLanguages }) {
|
|
660
|
+
return __awaiter(this, arguments, void 0, function* ({ selectedIds, selectedLanguages, adminUser }) {
|
|
548
661
|
const needToTranslateByLang = {};
|
|
549
662
|
const translations = yield this.adminforth.resource(this.resourceConfig.resourceId).list(Filters.IN(this.primaryKeyFieldName, selectedIds));
|
|
550
663
|
const languagesToProcess = selectedLanguages || this.options.supportedLanguages;
|
|
@@ -559,6 +672,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
559
672
|
needToTranslateByLang[lang] = [];
|
|
560
673
|
}
|
|
561
674
|
needToTranslateByLang[lang].push({
|
|
675
|
+
id: translation[this.primaryKeyFieldName],
|
|
562
676
|
'en_string': translation[this.enFieldName],
|
|
563
677
|
category: translation[this.options.categoryFieldName],
|
|
564
678
|
});
|
|
@@ -566,34 +680,25 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
566
680
|
}
|
|
567
681
|
}
|
|
568
682
|
const updateStrings = {};
|
|
569
|
-
|
|
570
|
-
let totalTranslated = [];
|
|
683
|
+
let generationTasksInitialData = [];
|
|
571
684
|
yield Promise.all(Object.entries(needToTranslateByLang).map((_a) => __awaiter(this, [_a], void 0, function* ([lang, strings]) {
|
|
572
685
|
// first translate without plurals
|
|
573
686
|
const stringsWithoutPlurals = strings.filter(s => !s.en_string.includes('|'));
|
|
574
|
-
const
|
|
687
|
+
const noPluralTranslationsTasks = yield this.getTranslateToLangTasks(lang, stringsWithoutPlurals, false, translations, updateStrings, needToTranslateByLang, adminUser);
|
|
575
688
|
const stringsWithPlurals = strings.filter(s => s.en_string.includes('|'));
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
})));
|
|
579
|
-
yield Promise.all(Object.entries(updateStrings).map((_a) => __awaiter(this, [_a], void 0, function* ([_, { updates, strId }]) {
|
|
580
|
-
// get old full record
|
|
581
|
-
const oldRecord = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.primaryKeyFieldName, strId)]);
|
|
582
|
-
// because this will translate all languages, we can set completedLangs to all languages
|
|
583
|
-
const futureCompletedFieldValue = yield this.computeCompletedFieldValue(Object.assign(Object.assign({}, oldRecord), updates));
|
|
584
|
-
yield this.adminforth.resource(this.resourceConfig.resourceId).update(strId, Object.assign(Object.assign({}, updates), { [this.options.completedFieldName]: futureCompletedFieldValue }));
|
|
689
|
+
const pluralTranslationsTasks = yield this.getTranslateToLangTasks(lang, stringsWithPlurals, true, translations, updateStrings, needToTranslateByLang, adminUser);
|
|
690
|
+
generationTasksInitialData = generationTasksInitialData.concat(noPluralTranslationsTasks || []).concat(pluralTranslationsTasks || []);
|
|
585
691
|
})));
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
return new Set(totalTranslated).size;
|
|
692
|
+
const totalTranslationTokenCost = generationTasksInitialData.reduce((a, b) => { var _a; return a + (((_a = b.state) === null || _a === void 0 ? void 0 : _a.promptCost) || 0); }, 0);
|
|
693
|
+
const backgroundJobsPlugin = this.adminforth.getPluginByClassName('BackgroundJobsPlugin');
|
|
694
|
+
const jobId = yield backgroundJobsPlugin.startNewJob(`Translate ${selectedIds.length} items`, //job name
|
|
695
|
+
adminUser, // adminuser
|
|
696
|
+
generationTasksInitialData, //initial tasks
|
|
697
|
+
'translation_job_handler');
|
|
698
|
+
afLogger.info(`Started background job with ID ${jobId} `);
|
|
699
|
+
yield backgroundJobsPlugin.setJobField(jobId, 'totalTranslationTokenCost', totalTranslationTokenCost);
|
|
700
|
+
yield backgroundJobsPlugin.setJobField(jobId, 'totalUsedTokens', 0);
|
|
701
|
+
return jobId;
|
|
597
702
|
});
|
|
598
703
|
}
|
|
599
704
|
processExtractedMessages(adminforth, filePath) {
|
|
@@ -645,6 +750,41 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
645
750
|
validateConfigAfterDiscover(adminforth, resourceConfig) {
|
|
646
751
|
// optional method where you can safely check field types after database discovery was performed
|
|
647
752
|
// ensure each trFieldName (apart from enFieldName) is nullable column of type string
|
|
753
|
+
const backgroundJobsPlugin = adminforth.getPluginByClassName('BackgroundJobsPlugin');
|
|
754
|
+
if (!backgroundJobsPlugin) {
|
|
755
|
+
throw new Error(`BackgroundJobsPlugin is required for ${this.constructor.name} to work, please add it to your plugins`);
|
|
756
|
+
}
|
|
757
|
+
backgroundJobsPlugin.registerTaskHandler({
|
|
758
|
+
// job handler name
|
|
759
|
+
jobHandlerName: 'translation_job_handler',
|
|
760
|
+
//handler function
|
|
761
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ jobId, setTaskStateField, getTaskStateField }) {
|
|
762
|
+
const initialState = yield getTaskStateField();
|
|
763
|
+
if (initialState.prompt && initialState.strings && initialState.translations && initialState.updateStrings && initialState.lang && initialState.failedToTranslate && initialState.needToTranslateByLang) {
|
|
764
|
+
yield this.generateAndSaveBunch(initialState.prompt, initialState.strings, initialState.translations, initialState.updateStrings, initialState.lang, initialState.failedToTranslate, initialState.needToTranslateByLang, jobId, initialState.promptCost);
|
|
765
|
+
}
|
|
766
|
+
afLogger.debug(`Translation task for language ${initialState.lang} completed.`);
|
|
767
|
+
const stateToSave = {
|
|
768
|
+
taskName: initialState.taskName,
|
|
769
|
+
lang: initialState.lang,
|
|
770
|
+
failedToTranslate: initialState.failedToTranslate,
|
|
771
|
+
};
|
|
772
|
+
yield setTaskStateField(stateToSave);
|
|
773
|
+
this.adminforth.websocket.publish('/translation_progress', {});
|
|
774
|
+
if (initialState.failedToTranslate.length > 0) {
|
|
775
|
+
afLogger.error(`Failed to translate some strings for language ${initialState.lang} in plugin ${this.constructor.name}:, ${initialState.failedToTranslate}`);
|
|
776
|
+
throw new Error(`Failed to translate some strings for language ${initialState.lang}, check job details for more info`);
|
|
777
|
+
}
|
|
778
|
+
}),
|
|
779
|
+
//limit of tasks, that are running in parallel
|
|
780
|
+
parallelLimit: this.options.parallelTranslationLimit || 20,
|
|
781
|
+
});
|
|
782
|
+
backgroundJobsPlugin.registerTaskDetailsComponent({
|
|
783
|
+
jobHandlerName: 'translation_job_handler', // Handler name
|
|
784
|
+
component: {
|
|
785
|
+
file: this.componentPath('TranslationJobViewComponent.vue') //custom component for the job details
|
|
786
|
+
},
|
|
787
|
+
});
|
|
648
788
|
if (this.options.completeAdapter) {
|
|
649
789
|
this.options.completeAdapter.validate();
|
|
650
790
|
}
|
|
@@ -905,29 +1045,75 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
905
1045
|
method: 'POST',
|
|
906
1046
|
path: `/plugin/${this.pluginInstanceId}/translate-selected-to-languages`,
|
|
907
1047
|
noAuth: false,
|
|
908
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, tr }) {
|
|
1048
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, tr, adminUser }) {
|
|
909
1049
|
const selectedLanguages = body.selectedLanguages;
|
|
910
1050
|
const selectedIds = body.selectedIds;
|
|
911
|
-
|
|
912
|
-
try {
|
|
913
|
-
translatedCount = yield this.bulkTranslate({ selectedIds, selectedLanguages });
|
|
914
|
-
}
|
|
915
|
-
catch (e) {
|
|
916
|
-
process.env.HEAVY_DEBUG && console.error('🪲⛔ bulkTranslate error', e);
|
|
917
|
-
if (e instanceof AiTranslateError) {
|
|
918
|
-
return { ok: false, error: e.message };
|
|
919
|
-
}
|
|
920
|
-
throw e;
|
|
921
|
-
}
|
|
922
|
-
this.updateUntranslatedMenuBadge();
|
|
1051
|
+
const jobId = yield this.bulkTranslate({ selectedIds, selectedLanguages, adminUser });
|
|
923
1052
|
return {
|
|
924
1053
|
ok: true,
|
|
925
|
-
|
|
926
|
-
successMessage: yield tr(`Translated {count} items`, 'backend', {
|
|
927
|
-
count: translatedCount,
|
|
928
|
-
}),
|
|
1054
|
+
jobId: jobId,
|
|
929
1055
|
};
|
|
930
1056
|
})
|
|
931
1057
|
});
|
|
1058
|
+
server.endpoint({
|
|
1059
|
+
method: 'POST',
|
|
1060
|
+
path: `/plugin/${this.pluginInstanceId}/get_filtered_ids`,
|
|
1061
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers, query, cookies, requestUrl }) {
|
|
1062
|
+
var _b, _c;
|
|
1063
|
+
const resource = this.resourceConfig;
|
|
1064
|
+
for (const hook of ((_c = (_b = resource.hooks) === null || _b === void 0 ? void 0 : _b.list) === null || _c === void 0 ? void 0 : _c.beforeDatasourceRequest) || []) {
|
|
1065
|
+
const filterTools = filtersTools.get(body);
|
|
1066
|
+
body.filtersTools = filterTools;
|
|
1067
|
+
const resp = yield hook({
|
|
1068
|
+
resource,
|
|
1069
|
+
query: body,
|
|
1070
|
+
adminUser,
|
|
1071
|
+
//@ts-ignore
|
|
1072
|
+
filtersTools: filterTools,
|
|
1073
|
+
extra: {
|
|
1074
|
+
body, query, headers, cookies, requestUrl
|
|
1075
|
+
},
|
|
1076
|
+
adminforth: this.adminforth,
|
|
1077
|
+
});
|
|
1078
|
+
if (!resp || (!resp.ok && !resp.error)) {
|
|
1079
|
+
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
1080
|
+
}
|
|
1081
|
+
if (resp.error) {
|
|
1082
|
+
return { error: resp.error };
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
const filters = body.filters;
|
|
1086
|
+
const normalizedFilters = { operator: AdminForthFilterOperators.AND, subFilters: [] };
|
|
1087
|
+
if (filters) {
|
|
1088
|
+
if (typeof filters !== 'object') {
|
|
1089
|
+
throw new Error(`Filter should be an array or an object`);
|
|
1090
|
+
}
|
|
1091
|
+
if (Array.isArray(filters)) {
|
|
1092
|
+
// if filters are an array, they will be connected with "AND" operator by default
|
|
1093
|
+
normalizedFilters.subFilters = filters;
|
|
1094
|
+
}
|
|
1095
|
+
else if (filters.field) {
|
|
1096
|
+
// assume filter is a SingleFilter
|
|
1097
|
+
normalizedFilters.subFilters = [filters];
|
|
1098
|
+
}
|
|
1099
|
+
else if (filters.subFilters) {
|
|
1100
|
+
// assume filter is a AndOr filter
|
|
1101
|
+
normalizedFilters.operator = filters.operator;
|
|
1102
|
+
normalizedFilters.subFilters = filters.subFilters;
|
|
1103
|
+
}
|
|
1104
|
+
else {
|
|
1105
|
+
// wrong filter
|
|
1106
|
+
throw new Error(`Wrong filter object value: ${JSON.stringify(filters)}`);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
const records = yield this.adminforth.resource(this.resourceConfig.resourceId).list(normalizedFilters);
|
|
1110
|
+
if (!records) {
|
|
1111
|
+
return { ok: true, recordIds: [] };
|
|
1112
|
+
}
|
|
1113
|
+
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
1114
|
+
const recordIds = records.map(record => record[primaryKeyColumn.name]);
|
|
1115
|
+
return { ok: true, recordIds };
|
|
1116
|
+
})
|
|
1117
|
+
});
|
|
932
1118
|
}
|
|
933
1119
|
}
|