@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/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.beforeActionButtons) {
412
- resourceConfig.options.pageInjections.list.beforeActionButtons = [];
419
+ if (!resourceConfig.options.pageInjections.list.threeDotsDropdownItems) {
420
+ resourceConfig.options.pageInjections.list.threeDotsDropdownItems = [];
413
421
  }
414
- resourceConfig.options.pageInjections.list.beforeActionButtons.push(pageInjection);
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
- translateToLang(langIsoCode_1, strings_1) {
440
- return __awaiter(this, arguments, void 0, function* (langIsoCode, strings, plurals = false, translations, updateStrings = {}) {
441
- var _a;
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: jsonSchemaRequired,
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; //.split("```json")[1].split("```")[0];
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
- return [];
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
- return [];
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
- return Object.keys(updateStrings);
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
- const langsInvolved = new Set(Object.keys(needToTranslateByLang));
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 noPluralKeys = yield this.translateToLang(lang, stringsWithoutPlurals, false, translations, updateStrings);
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 pluralKeys = yield this.translateToLang(lang, stringsWithPlurals, true, translations, updateStrings);
577
- totalTranslated = totalTranslated.concat(noPluralKeys, pluralKeys);
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
- for (const lang of langsInvolved) {
587
- const categoriesInvolved = new Set();
588
- for (const { enStr, category } of Object.values(updateStrings)) {
589
- categoriesInvolved.add(category);
590
- yield this.cache.clear(`${this.resourceConfig.resourceId}:${category}:${lang}:${enStr}`);
591
- }
592
- for (const category of categoriesInvolved) {
593
- yield this.cache.clear(`${this.resourceConfig.resourceId}:${category}:${lang}`);
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
- let translatedCount = 0;
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
- error: undefined,
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
  }