@adminforth/i18n 1.10.0 → 2.0.0

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/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- import AdminForth, { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes, RAMLock } from "adminforth";
2
- import type { IAdminForth, IHttpServer, AdminForthComponentDeclaration, AdminForthResourceColumn, AdminForthResource, BeforeLoginConfirmationFunction, AdminForthConfigMenuItem } from "adminforth";
1
+ import AdminForth, { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes, RAMLock, filtersTools, AdminForthFilterOperators } from "adminforth";
2
+ import type { IAdminForth, IHttpServer, AdminForthComponentDeclaration, AdminForthResourceColumn, AdminForthResource, BeforeLoginConfirmationFunction, AdminForthConfigMenuItem, AdminUser } from "adminforth";
3
3
  import type { PluginOptions, SupportedLanguage } from './types.js';
4
4
  import iso6391 from 'iso-639-1';
5
5
  import { iso31661Alpha2ToAlpha3 } from 'iso-3166';
@@ -9,6 +9,9 @@ import chokidar from 'chokidar';
9
9
  import { AsyncQueue } from '@sapphire/async-queue';
10
10
  import getFlagEmoji from 'country-flag-svg';
11
11
  import { parse } from 'bcp-47';
12
+ import pLimit from 'p-limit';
13
+ import { randomUUID } from 'crypto';
14
+ import { afLogger } from "adminforth";
12
15
 
13
16
  const processFrontendMessagesQueue = new AsyncQueue();
14
17
 
@@ -61,13 +64,19 @@ function isValidSupportedLanguageTag(langCode: SupportedLanguage): boolean {
61
64
  }
62
65
  }
63
66
  );
64
- const isValid = schema && schema.language && schema.language.length === 2;
65
- return !!isValid;
67
+ const isValid = schema.language.length === 2;
68
+ return isValid;
66
69
  } catch (e) {
67
70
  return false;
68
71
  }
69
72
  }
70
73
 
74
+ interface IFailedTranslation {
75
+ lang: SupportedLanguage;
76
+ en_string: string;
77
+ failedReason: string;
78
+ }
79
+
71
80
 
72
81
  interface ICachingAdapter {
73
82
  get(key: string): Promise<any>;
@@ -165,6 +174,15 @@ export default class I18nPlugin extends AdminForthPlugin {
165
174
  async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
166
175
  super.modifyResourceConfig(adminforth, resourceConfig);
167
176
 
177
+ if (!this.adminforth.config.componentsToExplicitRegister) {
178
+ this.adminforth.config.componentsToExplicitRegister = [];
179
+ }
180
+ this.adminforth.config.componentsToExplicitRegister.push(
181
+ {
182
+ file: this.componentPath('TranslationJobViewComponent.vue')
183
+ }
184
+ );
185
+
168
186
  // validate each supported language: ISO 639-1 or BCP-47 with region (e.g., en-GB)
169
187
  this.options.supportedLanguages.forEach((lang) => {
170
188
  if (!isValidSupportedLanguageTag(lang)) {
@@ -463,12 +481,11 @@ export default class I18nPlugin extends AdminForthPlugin {
463
481
  if (!resourceConfig.options.pageInjections.list) {
464
482
  resourceConfig.options.pageInjections.list = {};
465
483
  }
466
- if (!resourceConfig.options.pageInjections.list.beforeActionButtons) {
467
- resourceConfig.options.pageInjections.list.beforeActionButtons = [];
484
+ if (!resourceConfig.options.pageInjections.list.threeDotsDropdownItems) {
485
+ resourceConfig.options.pageInjections.list.threeDotsDropdownItems = [];
468
486
  }
469
487
 
470
- (resourceConfig.options.pageInjections.list.beforeActionButtons as AdminForthComponentDeclaration[]).push(pageInjection);
471
-
488
+ (resourceConfig.options.pageInjections.list.threeDotsDropdownItems as AdminForthComponentDeclaration[]).push(pageInjection);
472
489
 
473
490
  // if there is menu item with resourceId, add .badge function showing number of untranslated strings
474
491
  const addBadgeCountToMenuItem = (menuItem: AdminForthConfigMenuItem) => {
@@ -494,51 +511,19 @@ export default class I18nPlugin extends AdminForthPlugin {
494
511
  });
495
512
  }
496
513
 
497
- async translateToLang (
498
- langIsoCode: SupportedLanguage,
499
- strings: { en_string: string, category: string }[],
500
- plurals=false,
501
- translations: any[],
502
- updateStrings: Record<string, { updates: any, category: string, strId: string, enStr: string, translatedStr: string }> = {}
503
- ): Promise<string[]> {
504
- const maxKeysInOneReq = 10;
505
- if (strings.length === 0) {
506
- return [];
507
- }
508
-
509
- const replacedLanguageCodeForTranslations = this.options.translateLangAsBCP47Code && langIsoCode.length === 2 ? this.options.translateLangAsBCP47Code[langIsoCode as any] : null;
510
- if (strings.length > maxKeysInOneReq) {
511
- let totalTranslated = [];
512
- for (let i = 0; i < strings.length; i += maxKeysInOneReq) {
513
- const slicedStrings = strings.slice(i, i + maxKeysInOneReq);
514
- process.env.HEAVY_DEBUG && console.log('🪲🔪slicedStrings len', slicedStrings.length);
515
- const madeKeys = await this.translateToLang(langIsoCode, slicedStrings, plurals, translations, updateStrings);
516
- totalTranslated = totalTranslated.concat(madeKeys);
517
- }
518
- return totalTranslated;
519
- }
520
- const langCode = replacedLanguageCodeForTranslations ? replacedLanguageCodeForTranslations : langIsoCode;
521
- const lang = langIsoCode;
522
- const primaryLang = getPrimaryLanguageCode(lang);
523
- const langName = iso6391.getName(primaryLang);
524
- const requestSlavicPlurals = Object.keys(SLAVIC_PLURAL_EXAMPLES).includes(primaryLang) && plurals;
525
- const region = String(lang).split('-')[1]?.toUpperCase() || '';
526
- const prompt = `
527
- 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.
528
- ${region ? `Use the regional conventions for ${langCode} (region ${region}), including spelling, punctuation, and formatting.` : ''}
529
- ${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]}"` : ''}
530
- 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:
531
-
532
- \`\`\`json
533
- ${
534
- JSON.stringify(strings.reduce((acc: object, s: { en_string: string }): object => {
535
- acc[s.en_string] = '';
536
- return acc;
537
- }, {}), null, 2)
538
- }
539
- \`\`\`
540
- `;
541
-
514
+ async generateAndSaveBunch (
515
+ prompt: string,
516
+ strings: { en_string: string, category: string }[],
517
+ translations: any[],
518
+ updateStrings: Record<string, { updates: any, category: string, strId: string, enStr: string, translatedStr: string }> = {},
519
+ lang: string,
520
+ failedToTranslate: IFailedTranslation[],
521
+ needToTranslateByLang: Record<string, any> = {},
522
+ jobId: string,
523
+ promptCost: number,
524
+ ): Promise<void>{
525
+
526
+ // return [];
542
527
  const jsonSchemaProperties = {};
543
528
  strings.forEach(s => {
544
529
  jsonSchemaProperties[s.en_string] = {
@@ -548,7 +533,7 @@ export default class I18nPlugin extends AdminForthPlugin {
548
533
  });
549
534
 
550
535
  const jsonSchemaRequired = strings.map(s => s.en_string);
551
-
536
+ const dedupRequired = Array.from(new Set(jsonSchemaRequired));
552
537
  // call OpenAI
553
538
  const resp = await this.options.completeAdapter.complete(
554
539
  prompt,
@@ -560,36 +545,56 @@ export default class I18nPlugin extends AdminForthPlugin {
560
545
  schema: {
561
546
  type: "object",
562
547
  properties: jsonSchemaProperties,
563
- required: jsonSchemaRequired,
548
+ required: dedupRequired,
564
549
  },
565
550
  },
566
551
  }
567
552
  );
568
-
553
+
569
554
  process.env.HEAVY_DEBUG && console.log(`🪲🔪LLM resp >> ${prompt.length}, <<${resp.content.length} :\n\n`, JSON.stringify(resp));
570
555
 
571
556
  if (resp.error) {
572
557
  throw new AiTranslateError(resp.error);
573
558
  }
574
559
 
575
- // parse response like
576
- // Here are the translations for the strings you provided:
577
- // ```json
578
- // [{"live": "canlı"}, {"Table Games": "Masa Oyunları"}]
579
- // ```
580
560
  let res;
581
561
  try {
582
- res = resp.content//.split("```json")[1].split("```")[0];
562
+ res = resp.content;
583
563
  } catch (e) {
584
564
  console.error(`Error in parsing LLM resp: ${resp}\n Prompt was: ${prompt}\n Resp was: ${JSON.stringify(resp)}`, );
585
- return [];
565
+ strings.forEach(s => {
566
+ failedToTranslate.push({
567
+ lang: lang as SupportedLanguage,
568
+ en_string: s.en_string,
569
+ failedReason: "Error in parsing LLM response"
570
+ });
571
+ });
572
+ return null;
586
573
  }
587
574
 
575
+ const backgroundJobsPlugin = this.adminforth.getPluginByClassName<any>('BackgroundJobsPlugin');
576
+
577
+ backgroundJobsPlugin.updateJobFieldsAtomicly(jobId, async () => {
578
+ // 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.
579
+ // don't do long awaits in this callback, since it has exclusive lock.
580
+ let totalUsedTokens = await backgroundJobsPlugin.getJobField(jobId, 'totalUsedTokens');
581
+ totalUsedTokens += promptCost;
582
+ await backgroundJobsPlugin.setJobField(jobId, 'totalUsedTokens', totalUsedTokens);
583
+ })
584
+
585
+
588
586
  try {
589
587
  res = JSON.parse(res);
590
588
  } catch (e) {
591
589
  console.error(`Error in parsing LLM resp json: ${resp}\n Prompt was: ${prompt}\n Resp was: ${JSON.stringify(resp)}`, );
592
- return [];
590
+ strings.forEach(s => {
591
+ failedToTranslate.push({
592
+ lang: lang as SupportedLanguage,
593
+ en_string: s.en_string,
594
+ failedReason: "Error in parsing LLM response JSON"
595
+ });
596
+ });
597
+ return null;
593
598
  }
594
599
 
595
600
 
@@ -598,7 +603,6 @@ export default class I18nPlugin extends AdminForthPlugin {
598
603
  // might be several with same en_string
599
604
  for (const translation of translationsTargeted) {
600
605
  //translation[this.trFieldNames[lang]] = translatedStr;
601
- // process.env.HEAVY_DEBUG && console.log(`🪲translated to ${lang} ${translation.en_string}, ${translatedStr}`)
602
606
  if (!updateStrings[translation[this.primaryKeyFieldName]]) {
603
607
 
604
608
  updateStrings[translation[this.primaryKeyFieldName]] = {
@@ -620,16 +624,181 @@ export default class I18nPlugin extends AdminForthPlugin {
620
624
  }
621
625
  }
622
626
 
623
- return Object.keys(updateStrings);
627
+ const langsInvolved = new Set(Object.keys(needToTranslateByLang));
628
+
629
+ // here we need to save updateStrings
630
+ await Promise.all(
631
+ Object.entries(updateStrings).map(
632
+ async ([_, { updates, strId }]: [string, { updates: any, category: string, strId: string }]) => {
633
+ // get old full record
634
+ const oldRecord = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.primaryKeyFieldName, strId)]);
635
+
636
+ // because this will translate all languages, we can set completedLangs to all languages
637
+ const futureCompletedFieldValue = await this.computeCompletedFieldValue({ ...oldRecord, ...updates });
638
+
639
+ await this.adminforth.resource(this.resourceConfig.resourceId).update(strId, {
640
+ ...updates,
641
+ [this.options.completedFieldName]: futureCompletedFieldValue,
642
+ });
643
+ }
644
+ )
645
+ );
646
+
647
+
648
+ for (const lang of langsInvolved) {
649
+ const categoriesInvolved = new Set();
650
+ for (const { enStr, category } of Object.values(updateStrings)) {
651
+ categoriesInvolved.add(category);
652
+ await this.cache.clear(`${this.resourceConfig.resourceId}:${category}:${lang}:${enStr}`);
653
+ }
654
+ for (const category of categoriesInvolved) {
655
+ await this.cache.clear(`${this.resourceConfig.resourceId}:${category}:${lang}`);
656
+ }
657
+ }
658
+
624
659
  }
625
660
 
626
- // returns translated count
627
- async bulkTranslate({ selectedIds, selectedLanguages }: { selectedIds: string[], selectedLanguages?: SupportedLanguage[] }): Promise<number> {
661
+ async getTranslateToLangTasks (
662
+ langIsoCode: SupportedLanguage,
663
+ strings: { id: string, en_string: string, category: string }[],
664
+ plurals=false,
665
+ translations: any[],
666
+ updateStrings: Record<string, { updates: any, category: string, strId: string, enStr: string, translatedStr: string }> = {},
667
+ needToTranslateByLang: Record<string, any> = {},
668
+ adminUser: AdminUser,
669
+ ): Promise<any> {
670
+
671
+ const maxInputTokens = this.options.inputTokensPerBatch ?? 30000;
672
+
673
+ const limit = pLimit(30);
674
+ const enStringsTokenLengthCache: Record<string, any> = {};
675
+
676
+
677
+ const tokenLengthPerString = async ({ str, id }: { str: string; id: string }): Promise<void> => {
678
+ const objectToPush = {
679
+ en_string: str,
680
+ numOfTokens: await this.options.completeAdapter.measureTokensCount(`"${str}":"", \n`)
681
+ };
682
+ enStringsTokenLengthCache[id] = objectToPush;
683
+ }
684
+ const promises = strings.map(s => limit(() => tokenLengthPerString({ str: s.en_string, id: s.id })));
685
+
686
+ await Promise.all(promises);
687
+ if (strings.length === 0) {
688
+ return;
689
+ }
690
+
691
+ const replacedLanguageCodeForTranslations = this.options.translateLangAsBCP47Code && langIsoCode.length === 2 ? this.options.translateLangAsBCP47Code[langIsoCode as any] : null;
692
+ const langSchema = parse(String(langIsoCode),
693
+ {
694
+ normalize: true,
695
+ }
696
+ );
697
+ const langCode = replacedLanguageCodeForTranslations ? replacedLanguageCodeForTranslations : langIsoCode;
698
+ const lang = langIsoCode;
699
+ const primaryLang = getPrimaryLanguageCode(lang);
700
+ const langName = iso6391.getName(primaryLang);
701
+ const requestSlavicPlurals = Object.keys(SLAVIC_PLURAL_EXAMPLES).includes(primaryLang) && plurals;
702
+ const region = langSchema.region;
703
+ const basePrompt = `
704
+ 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.
705
+
706
+
707
+ ${region ? `Use the regional conventions for ${langCode} (region ${region}), including spelling, punctuation, and formatting.` : ''}
708
+ ${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]}"` : ''}
709
+ 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:
710
+ \`\`\`json
711
+ {
628
712
 
713
+ }
714
+ \`\`\`
715
+ `;
716
+
717
+ const failedToTranslate: IFailedTranslation[] = [];
718
+ const basePromptTokenLength = await this.options.completeAdapter.measureTokensCount(basePrompt);
719
+ const allowedTokensAmountForFields = maxInputTokens - basePromptTokenLength;
720
+ const stringsToTranslate: Record<string, { id: string; en_string: string; category: string }> = Object.fromEntries(strings.map(s => [s.id, s]));
721
+ const generationTasksInitialData = []
722
+ while (Object.keys(stringsToTranslate).length !== 0) {
723
+ const stringBanch = [];
724
+ const stringIdsInBatch = [];
725
+ let banchTokens = 0;
726
+ for (const id of Object.keys(stringsToTranslate)) {
727
+ const { en_string } = stringsToTranslate[id];
728
+ const numberOfTokensForString = enStringsTokenLengthCache[id]?.numOfTokens || 0;
729
+ if( banchTokens + numberOfTokensForString <= allowedTokensAmountForFields ) {
730
+ stringBanch.push( en_string );
731
+ stringIdsInBatch.push(id);
732
+ banchTokens += numberOfTokensForString;
733
+ } else {
734
+ continue;
735
+ }
736
+ }
737
+ if ( stringBanch.length === 0 ) {
738
+ Object.values(stringsToTranslate).forEach(s => {
739
+ failedToTranslate.push({
740
+ lang,
741
+ en_string: s.en_string,
742
+ failedReason: "Not enough input generation tokens"
743
+ });
744
+ generationTasksInitialData.push(
745
+ {
746
+ state: {
747
+ failedToTranslate,
748
+ }
749
+ }
750
+ )
751
+ });
752
+ break;
753
+ }
754
+
755
+ for ( const id of stringIdsInBatch) {
756
+ delete stringsToTranslate[id];
757
+ }
758
+
759
+ const promptToGenerate = basePrompt.split(`\`\`\`json`)[0] +
760
+ `\`\`\`json
761
+ {
762
+ ${
763
+ stringBanch.map(s => `"${s}": ""`).join(",\n")
764
+ }
765
+ }
766
+ \`\`\``;
767
+ const stringBanchCopy = [...stringBanch];
768
+ generationTasksInitialData.push(
769
+ {
770
+ state: {
771
+ taskName: `Translate ${strings.length} strings`,
772
+ prompt: promptToGenerate,
773
+ strings: strings.filter(s => stringBanchCopy.includes(s.en_string)),
774
+ translations: translations.filter(t => stringBanchCopy.includes(t.en_string)),
775
+ updateStrings,
776
+ lang,
777
+ failedToTranslate,
778
+ needToTranslateByLang,
779
+ promptCost: basePromptTokenLength + banchTokens,
780
+ }
781
+ }
782
+ )
783
+ }
784
+
785
+ return generationTasksInitialData;
786
+ }
787
+
788
+
789
+ // returns translated count
790
+ async bulkTranslate({ selectedIds, selectedLanguages, adminUser }:
791
+ {
792
+ selectedIds: string[],
793
+ selectedLanguages?: SupportedLanguage[],
794
+ adminUser: AdminUser,
795
+ }): Promise<string>
796
+ {
629
797
  const needToTranslateByLang : Partial<
630
798
  Record<
631
799
  SupportedLanguage,
632
800
  {
801
+ id: string;
633
802
  en_string: string;
634
803
  category: string;
635
804
  }[]
@@ -649,6 +818,7 @@ export default class I18nPlugin extends AdminForthPlugin {
649
818
  needToTranslateByLang[lang] = [];
650
819
  }
651
820
  needToTranslateByLang[lang].push({
821
+ id: translation[this.primaryKeyFieldName],
652
822
  'en_string': translation[this.enFieldName],
653
823
  category: translation[this.options.categoryFieldName],
654
824
  })
@@ -665,58 +835,39 @@ export default class I18nPlugin extends AdminForthPlugin {
665
835
  }> = {};
666
836
 
667
837
 
668
- const langsInvolved = new Set(Object.keys(needToTranslateByLang));
669
-
670
- let totalTranslated = [];
671
-
838
+ let generationTasksInitialData = [];
672
839
  await Promise.all(
673
840
  Object.entries(needToTranslateByLang).map(
674
- async ([lang, strings]: [SupportedLanguage, { en_string: string, category: string }[]]) => {
841
+ async ([lang, strings]: [SupportedLanguage, { id: string, en_string: string, category: string }[]]) => {
675
842
  // first translate without plurals
676
843
  const stringsWithoutPlurals = strings.filter(s => !s.en_string.includes('|'));
677
- const noPluralKeys = await this.translateToLang(lang, stringsWithoutPlurals, false, translations, updateStrings);
678
-
844
+ const noPluralTranslationsTasks = await this.getTranslateToLangTasks(lang, stringsWithoutPlurals, false, translations, updateStrings, needToTranslateByLang, adminUser);
679
845
 
680
846
  const stringsWithPlurals = strings.filter(s => s.en_string.includes('|'));
681
-
682
- const pluralKeys = await this.translateToLang(lang, stringsWithPlurals, true, translations, updateStrings);
683
-
684
- totalTranslated = totalTranslated.concat(noPluralKeys, pluralKeys);
847
+ const pluralTranslationsTasks = await this.getTranslateToLangTasks(lang, stringsWithPlurals, true, translations, updateStrings, needToTranslateByLang, adminUser);
848
+ generationTasksInitialData = generationTasksInitialData.concat(noPluralTranslationsTasks || []).concat(pluralTranslationsTasks || []);
685
849
  }
686
850
  )
687
851
  );
688
852
 
689
- await Promise.all(
690
- Object.entries(updateStrings).map(
691
- async ([_, { updates, strId }]: [string, { updates: any, category: string, strId: string }]) => {
692
- // get old full record
693
- const oldRecord = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.primaryKeyFieldName, strId)]);
694
-
695
- // because this will translate all languages, we can set completedLangs to all languages
696
- const futureCompletedFieldValue = await this.computeCompletedFieldValue({ ...oldRecord, ...updates });
697
853
 
698
- await this.adminforth.resource(this.resourceConfig.resourceId).update(strId, {
699
- ...updates,
700
- [this.options.completedFieldName]: futureCompletedFieldValue,
701
- });
702
- }
703
- )
854
+ const totalTranslationTokenCost = generationTasksInitialData.reduce((a, b) => a + (b.state?.promptCost || 0), 0);
855
+ const backgroundJobsPlugin = this.adminforth.getPluginByClassName<any>('BackgroundJobsPlugin');
856
+ const jobId = await backgroundJobsPlugin.startNewJob(
857
+ `Translate ${selectedIds.length} items`, //job name
858
+ adminUser, // adminuser
859
+ generationTasksInitialData, //initial tasks
860
+ 'translation_job_handler', //job handler name
704
861
  );
705
862
 
706
- for (const lang of langsInvolved) {
707
- const categoriesInvolved = new Set();
708
- for (const { enStr, category } of Object.values(updateStrings)) {
709
- categoriesInvolved.add(category);
710
- await this.cache.clear(`${this.resourceConfig.resourceId}:${category}:${lang}:${enStr}`);
711
- }
712
- for (const category of categoriesInvolved) {
713
- await this.cache.clear(`${this.resourceConfig.resourceId}:${category}:${lang}`);
714
- }
715
- }
863
+ afLogger.info(`Started background job with ID ${jobId} `);
864
+ await backgroundJobsPlugin.setJobField(jobId, 'totalTranslationTokenCost', totalTranslationTokenCost);
865
+ await backgroundJobsPlugin.setJobField(jobId, 'totalUsedTokens', 0);
716
866
 
717
- return new Set(totalTranslated).size;
867
+ return jobId
718
868
  }
719
869
 
870
+
720
871
  async processExtractedMessages(adminforth: IAdminForth, filePath: string) {
721
872
  await processFrontendMessagesQueue.wait();
722
873
  // messages file is in i18n-messages.json
@@ -768,6 +919,69 @@ export default class I18nPlugin extends AdminForthPlugin {
768
919
  validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
769
920
  // optional method where you can safely check field types after database discovery was performed
770
921
  // ensure each trFieldName (apart from enFieldName) is nullable column of type string
922
+ const backgroundJobsPlugin = adminforth.getPluginByClassName<any>('BackgroundJobsPlugin');
923
+
924
+ if (!backgroundJobsPlugin) {
925
+ throw new Error(`BackgroundJobsPlugin is required for ${this.constructor.name} to work, please add it to your plugins`);
926
+ }
927
+
928
+ backgroundJobsPlugin.registerTaskHandler({
929
+ // job handler name
930
+ jobHandlerName: 'translation_job_handler',
931
+ //handler function
932
+ handler: async ({ jobId, setTaskStateField, getTaskStateField }) => {
933
+ const initialState: {
934
+ taskName: string,
935
+ prompt?: string,
936
+ strings?: { en_string: string, category: string }[],
937
+ translations?: any[],
938
+ updateStrings?: Record<string, { updates: any, category: string, strId: string, enStr: string, translatedStr: string }>,
939
+ lang?: string,
940
+ failedToTranslate?: IFailedTranslation[],
941
+ needToTranslateByLang?: Record<string, any>,
942
+ promptCost?: number,
943
+ } = await getTaskStateField();
944
+
945
+ if ( initialState.prompt && initialState.strings && initialState.translations && initialState.updateStrings && initialState.lang && initialState.failedToTranslate && initialState.needToTranslateByLang) {
946
+ await this.generateAndSaveBunch(
947
+ initialState.prompt,
948
+ initialState.strings,
949
+ initialState.translations,
950
+ initialState.updateStrings,
951
+ initialState.lang,
952
+ initialState.failedToTranslate,
953
+ initialState.needToTranslateByLang,
954
+ jobId,
955
+ initialState.promptCost
956
+ );
957
+ }
958
+ afLogger.debug(`Translation task for language ${initialState.lang} completed.`);
959
+
960
+ const stateToSave = {
961
+ taskName: initialState.taskName,
962
+ lang: initialState.lang,
963
+ failedToTranslate: initialState.failedToTranslate,
964
+ }
965
+ await setTaskStateField(stateToSave);
966
+
967
+ this.adminforth.websocket.publish('/translation_progress', {});
968
+ if (initialState.failedToTranslate.length > 0) {
969
+ afLogger.error(`Failed to translate some strings for language ${initialState.lang} in plugin ${this.constructor.name}:, ${initialState.failedToTranslate}`);
970
+ throw new Error(`Failed to translate some strings for language ${initialState.lang}, check job details for more info`);
971
+ }
972
+ },
973
+ //limit of tasks, that are running in parallel
974
+ parallelLimit: this.options.parallelTranslationLimit || 20,
975
+ })
976
+
977
+ backgroundJobsPlugin.registerTaskDetailsComponent({
978
+ jobHandlerName: 'translation_job_handler', // Handler name
979
+ component: {
980
+ file: this.componentPath('TranslationJobViewComponent.vue') //custom component for the job details
981
+ },
982
+ })
983
+
984
+
771
985
  if (this.options.completeAdapter) {
772
986
  this.options.completeAdapter.validate();
773
987
  }
@@ -1071,31 +1285,81 @@ export default class I18nPlugin extends AdminForthPlugin {
1071
1285
  method: 'POST',
1072
1286
  path: `/plugin/${this.pluginInstanceId}/translate-selected-to-languages`,
1073
1287
  noAuth: false,
1074
- handler: async ({ body, tr }) => {
1288
+ handler: async ({ body, tr, adminUser }) => {
1075
1289
  const selectedLanguages = body.selectedLanguages;
1076
1290
  const selectedIds = body.selectedIds;
1291
+
1292
+ const jobId = await this.bulkTranslate({ selectedIds, selectedLanguages, adminUser });
1077
1293
 
1078
- let translatedCount = 0;
1079
- try {
1080
- translatedCount = await this.bulkTranslate({ selectedIds, selectedLanguages });
1081
- } catch (e) {
1082
- process.env.HEAVY_DEBUG && console.error('🪲⛔ bulkTranslate error', e);
1083
- if (e instanceof AiTranslateError) {
1084
- return { ok: false, error: e.message };
1085
- }
1086
- throw e;
1087
- }
1088
- this.updateUntranslatedMenuBadge();
1089
1294
  return {
1090
1295
  ok: true,
1091
- error: undefined,
1092
- successMessage: await tr(`Translated {count} items`, 'backend', {
1093
- count: translatedCount,
1094
- }),
1296
+ jobId: jobId,
1095
1297
  };
1096
1298
  }
1097
1299
  });
1098
1300
 
1301
+ server.endpoint({
1302
+ method: 'POST',
1303
+ path: `/plugin/${this.pluginInstanceId}/get_filtered_ids`,
1304
+ handler: async ({ body, adminUser, headers, query, cookies, requestUrl }) => {
1305
+ const resource = this.resourceConfig;
1306
+
1307
+ for (const hook of resource.hooks?.list?.beforeDatasourceRequest || []) {
1308
+ const filterTools = filtersTools.get(body);
1309
+ body.filtersTools = filterTools;
1310
+ const resp = await hook({
1311
+ resource,
1312
+ query: body,
1313
+ adminUser,
1314
+ //@ts-ignore
1315
+ filtersTools: filterTools,
1316
+ extra: {
1317
+ body, query, headers, cookies, requestUrl
1318
+ },
1319
+ adminforth: this.adminforth,
1320
+ });
1321
+ if (!resp || (!resp.ok && !resp.error)) {
1322
+ throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
1323
+ }
1324
+ if (resp.error) {
1325
+ return { error: resp.error };
1326
+ }
1327
+ }
1328
+ const filters = body.filters;
1329
+
1330
+ const normalizedFilters = { operator: AdminForthFilterOperators.AND, subFilters: [] };
1331
+ if (filters) {
1332
+ if (typeof filters !== 'object') {
1333
+ throw new Error(`Filter should be an array or an object`);
1334
+ }
1335
+ if (Array.isArray(filters)) {
1336
+ // if filters are an array, they will be connected with "AND" operator by default
1337
+ normalizedFilters.subFilters = filters;
1338
+ } else if (filters.field) {
1339
+ // assume filter is a SingleFilter
1340
+ normalizedFilters.subFilters = [filters];
1341
+ } else if (filters.subFilters) {
1342
+ // assume filter is a AndOr filter
1343
+ normalizedFilters.operator = filters.operator;
1344
+ normalizedFilters.subFilters = filters.subFilters;
1345
+ } else {
1346
+ // wrong filter
1347
+ throw new Error(`Wrong filter object value: ${JSON.stringify(filters)}`);
1348
+ }
1349
+ }
1350
+
1351
+ const records = await this.adminforth.resource(this.resourceConfig.resourceId).list(normalizedFilters);
1352
+ if (!records) {
1353
+ return { ok: true, recordIds: [] };
1354
+ }
1355
+ const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
1356
+
1357
+ const recordIds = records.map(record => record[primaryKeyColumn.name]);
1358
+
1359
+ return { ok: true, recordIds }
1360
+ }
1361
+ });
1362
+
1099
1363
  }
1100
1364
 
1101
1365
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/i18n",
3
- "version": "1.10.0",
3
+ "version": "2.0.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -35,7 +35,8 @@
35
35
  "country-flag-svg": "^1.0.19",
36
36
  "fs-extra": "^11.3.2",
37
37
  "iso-3166": "^4.3.0",
38
- "iso-639-1": "^3.1.3"
38
+ "iso-639-1": "^3.1.3",
39
+ "p-limit": "^7.3.0"
39
40
  },
40
41
  "peerDependencies": {
41
42
  "adminforth": "next"