@adminforth/i18n 2.1.2 → 2.2.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/build.log CHANGED
@@ -20,5 +20,5 @@ custom/package.json
20
20
  custom/pnpm-lock.yaml
21
21
  custom/tsconfig.json
22
22
 
23
- sent 45,592 bytes received 305 bytes 91,794.00 bytes/sec
24
- total size is 44,424 speedup is 0.97
23
+ sent 47,746 bytes received 305 bytes 96,102.00 bytes/sec
24
+ total size is 46,578 speedup is 0.97
@@ -12,6 +12,7 @@
12
12
  </div>
13
13
  <div class="grid grid-cols-3 gap-2">
14
14
  <div
15
+ v-if="!isFetchingTasks"
15
16
  class="bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border-gray-200 dark:border-gray-700 transition-all px-2 py-2 rounded-md border max-w-64 w-full flex items-center justify-between gap-2"
16
17
  v-for="(task, index) in translationTasks"
17
18
  :key="index"
@@ -30,25 +31,56 @@
30
31
  />
31
32
  </div>
32
33
  </div>
34
+ <div v-else class="col-span-3 flex items-center justify-center">
35
+ <Spinner class="w-10 h-10" />
36
+ </div>
37
+ </div>
38
+ <div v-if="page > 0 || hasNextPage" class="flex items-center justify-center gap-3 mt-4">
39
+ <Button
40
+ variant="secondary"
41
+ :disabled="page === 0 || isFetchingTasks"
42
+ @click="loadPage(page - 1)"
43
+ >
44
+ <IconAngleLeftOutline class="w-4 h-4" />
45
+ </Button>
46
+ <span class="text-sm text-gray-600 dark:text-gray-300">
47
+ {{ t('Page {page}', { page: page + 1 }) }}
48
+ </span>
49
+ <Button
50
+ variant="secondary"
51
+ :disabled="!hasNextPage || isFetchingTasks"
52
+ @click="loadPage(page + 1)"
53
+ >
54
+ <IconAngleRightOutline class="w-4 h-4" />
55
+ </Button>
33
56
  </div>
34
57
  </template>
35
58
 
36
59
 
37
60
  <script setup lang="ts">
38
61
  import { useI18n } from 'vue-i18n'
39
- import { onMounted, onUnmounted, ref } from 'vue';
62
+ import { computed, onMounted, onUnmounted, ref } from 'vue';
40
63
  import websocket from '@/websocket';
41
64
  import { getCountryCodeFromLangCode } from './langCommon';
42
65
  import { getCustomComponent } from '@/utils';
66
+ import { Button, Spinner } from '@/afcl';
67
+ import { IconAngleLeftOutline, IconAngleRightOutline } from '@iconify-prerendered/vue-flowbite';
43
68
 
44
69
  const { t } = useI18n();
45
70
 
46
71
  const translationTasks = ref<{state: Record<string, any>, status: string}[]>([]);
47
- const currentPaginationWindow = ref({limit: 25, offset: 0});
72
+ const isFetchingTasks = ref(false);
73
+ const page = ref(0);
74
+ const hasNextPage = ref(false);
75
+ const PAGE_SIZE = 24;
76
+ const MAX_RETRIES = 10; // Maximum number of retries for fetching tasks
77
+ const RETRY_DELAY_MS = 2000; // 2 seconds
78
+
79
+ const currentOffset = computed(() => page.value * PAGE_SIZE);
48
80
 
49
81
  const props = defineProps<{
50
82
  meta: any;
51
- getJobTasks: (limit?: number, offset?: number) => Promise<
83
+ getJobTasks: (limit?: number, offset?: number, fieldsToReturn?: string[]) => Promise<
52
84
  {state: Record<string, any>, status: string}[]>;
53
85
  job: {
54
86
  id: string;
@@ -64,15 +96,51 @@ const props = defineProps<{
64
96
  };
65
97
  }>();
66
98
 
99
+ async function loadPage(targetPage: number) {
100
+ if (targetPage < 0 || isFetchingTasks.value) {
101
+ return;
102
+ }
103
+ isFetchingTasks.value = true;
104
+ const retries = targetPage === 0 ? MAX_RETRIES : 1;
105
+ try {
106
+ for (let retry = 0; retry < retries; retry++) {
107
+ try {
108
+ const res = await props.getJobTasks(PAGE_SIZE, targetPage * PAGE_SIZE, ['lang', 'error', 'taskName', 'failedToTranslate']);
109
+ if (res && res.length > 0) {
110
+ translationTasks.value = res;
111
+ page.value = targetPage;
112
+ hasNextPage.value = res.length === PAGE_SIZE;
113
+ return;
114
+ }
115
+ } catch (error) {
116
+ console.error('Failed to fetch translation tasks, retrying...', error);
117
+ }
118
+ if (retry < retries - 1) {
119
+ await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
120
+ }
121
+ }
122
+ // nothing came back: either there are no tasks at all, or we went past the last page
123
+ if (targetPage === 0) {
124
+ translationTasks.value = [];
125
+ page.value = 0;
126
+ }
127
+ hasNextPage.value = false;
128
+ } finally {
129
+ isFetchingTasks.value = false;
130
+ }
131
+ }
132
+
67
133
  onMounted(async () => {
68
- translationTasks.value = await props.getJobTasks(currentPaginationWindow.value.limit, currentPaginationWindow.value.offset);
134
+ await loadPage(0);
135
+
69
136
  websocket.subscribe(`/background-jobs-task-update/${props.job.id}`, (data: { taskIndex: number, status?: string, state?: Record<string, any> }) => {
70
- if ( data.taskIndex <= currentPaginationWindow.value.offset + currentPaginationWindow.value.limit && data.taskIndex >= currentPaginationWindow.value.offset ) {
137
+ const indexOnPage = data.taskIndex - currentOffset.value;
138
+ if (indexOnPage >= 0 && indexOnPage < translationTasks.value.length) {
71
139
  if (data.state) {
72
- translationTasks.value[data.taskIndex].state = data.state;
140
+ translationTasks.value[indexOnPage].state = data.state;
73
141
  }
74
142
  if (data.status) {
75
- translationTasks.value[data.taskIndex].status = data.status;
143
+ translationTasks.value[indexOnPage].status = data.status;
76
144
  }
77
145
  }
78
146
  });
@@ -12,6 +12,7 @@
12
12
  </div>
13
13
  <div class="grid grid-cols-3 gap-2">
14
14
  <div
15
+ v-if="!isFetchingTasks"
15
16
  class="bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border-gray-200 dark:border-gray-700 transition-all px-2 py-2 rounded-md border max-w-64 w-full flex items-center justify-between gap-2"
16
17
  v-for="(task, index) in translationTasks"
17
18
  :key="index"
@@ -30,25 +31,56 @@
30
31
  />
31
32
  </div>
32
33
  </div>
34
+ <div v-else class="col-span-3 flex items-center justify-center">
35
+ <Spinner class="w-10 h-10" />
36
+ </div>
37
+ </div>
38
+ <div v-if="page > 0 || hasNextPage" class="flex items-center justify-center gap-3 mt-4">
39
+ <Button
40
+ variant="secondary"
41
+ :disabled="page === 0 || isFetchingTasks"
42
+ @click="loadPage(page - 1)"
43
+ >
44
+ <IconAngleLeftOutline class="w-4 h-4" />
45
+ </Button>
46
+ <span class="text-sm text-gray-600 dark:text-gray-300">
47
+ {{ t('Page {page}', { page: page + 1 }) }}
48
+ </span>
49
+ <Button
50
+ variant="secondary"
51
+ :disabled="!hasNextPage || isFetchingTasks"
52
+ @click="loadPage(page + 1)"
53
+ >
54
+ <IconAngleRightOutline class="w-4 h-4" />
55
+ </Button>
33
56
  </div>
34
57
  </template>
35
58
 
36
59
 
37
60
  <script setup lang="ts">
38
61
  import { useI18n } from 'vue-i18n'
39
- import { onMounted, onUnmounted, ref } from 'vue';
62
+ import { computed, onMounted, onUnmounted, ref } from 'vue';
40
63
  import websocket from '@/websocket';
41
64
  import { getCountryCodeFromLangCode } from './langCommon';
42
65
  import { getCustomComponent } from '@/utils';
66
+ import { Button, Spinner } from '@/afcl';
67
+ import { IconAngleLeftOutline, IconAngleRightOutline } from '@iconify-prerendered/vue-flowbite';
43
68
 
44
69
  const { t } = useI18n();
45
70
 
46
71
  const translationTasks = ref<{state: Record<string, any>, status: string}[]>([]);
47
- const currentPaginationWindow = ref({limit: 25, offset: 0});
72
+ const isFetchingTasks = ref(false);
73
+ const page = ref(0);
74
+ const hasNextPage = ref(false);
75
+ const PAGE_SIZE = 24;
76
+ const MAX_RETRIES = 10; // Maximum number of retries for fetching tasks
77
+ const RETRY_DELAY_MS = 2000; // 2 seconds
78
+
79
+ const currentOffset = computed(() => page.value * PAGE_SIZE);
48
80
 
49
81
  const props = defineProps<{
50
82
  meta: any;
51
- getJobTasks: (limit?: number, offset?: number) => Promise<
83
+ getJobTasks: (limit?: number, offset?: number, fieldsToReturn?: string[]) => Promise<
52
84
  {state: Record<string, any>, status: string}[]>;
53
85
  job: {
54
86
  id: string;
@@ -64,15 +96,51 @@ const props = defineProps<{
64
96
  };
65
97
  }>();
66
98
 
99
+ async function loadPage(targetPage: number) {
100
+ if (targetPage < 0 || isFetchingTasks.value) {
101
+ return;
102
+ }
103
+ isFetchingTasks.value = true;
104
+ const retries = targetPage === 0 ? MAX_RETRIES : 1;
105
+ try {
106
+ for (let retry = 0; retry < retries; retry++) {
107
+ try {
108
+ const res = await props.getJobTasks(PAGE_SIZE, targetPage * PAGE_SIZE, ['lang', 'error', 'taskName', 'failedToTranslate']);
109
+ if (res && res.length > 0) {
110
+ translationTasks.value = res;
111
+ page.value = targetPage;
112
+ hasNextPage.value = res.length === PAGE_SIZE;
113
+ return;
114
+ }
115
+ } catch (error) {
116
+ console.error('Failed to fetch translation tasks, retrying...', error);
117
+ }
118
+ if (retry < retries - 1) {
119
+ await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
120
+ }
121
+ }
122
+ // nothing came back: either there are no tasks at all, or we went past the last page
123
+ if (targetPage === 0) {
124
+ translationTasks.value = [];
125
+ page.value = 0;
126
+ }
127
+ hasNextPage.value = false;
128
+ } finally {
129
+ isFetchingTasks.value = false;
130
+ }
131
+ }
132
+
67
133
  onMounted(async () => {
68
- translationTasks.value = await props.getJobTasks(currentPaginationWindow.value.limit, currentPaginationWindow.value.offset);
134
+ await loadPage(0);
135
+
69
136
  websocket.subscribe(`/background-jobs-task-update/${props.job.id}`, (data: { taskIndex: number, status?: string, state?: Record<string, any> }) => {
70
- if ( data.taskIndex <= currentPaginationWindow.value.offset + currentPaginationWindow.value.limit && data.taskIndex >= currentPaginationWindow.value.offset ) {
137
+ const indexOnPage = data.taskIndex - currentOffset.value;
138
+ if (indexOnPage >= 0 && indexOnPage < translationTasks.value.length) {
71
139
  if (data.state) {
72
- translationTasks.value[data.taskIndex].state = data.state;
140
+ translationTasks.value[indexOnPage].state = data.state;
73
141
  }
74
142
  if (data.status) {
75
- translationTasks.value[data.taskIndex].status = data.status;
143
+ translationTasks.value[indexOnPage].status = data.status;
76
144
  }
77
145
  }
78
146
  });
package/dist/index.d.ts CHANGED
@@ -40,7 +40,10 @@ export default class I18nPlugin extends AdminForthPlugin {
40
40
  strId: string;
41
41
  enStr: string;
42
42
  translatedStr: string;
43
- }>, lang: string, failedToTranslate: IFailedTranslation[], needToTranslateByLang: Record<string, any>, jobId: string, promptCost: number): Promise<void>;
43
+ }>, lang: string, failedToTranslate: IFailedTranslation[], needToTranslateByLang: Record<string, any>, jobId: string, promptCost: number): Promise<{
44
+ ok: boolean;
45
+ error?: string;
46
+ }>;
44
47
  getTranslateToLangTasks(langIsoCode: SupportedLanguage, strings: {
45
48
  id: string;
46
49
  en_string: string;
package/dist/index.js CHANGED
@@ -496,7 +496,7 @@ export default class I18nPlugin extends AdminForthPlugin {
496
496
  });
497
497
  process.env.HEAVY_DEBUG && console.log(`🪲🔪LLM resp >> ${prompt.length}, <<${resp.content.length} :\n\n`, JSON.stringify(resp));
498
498
  if (resp.error) {
499
- throw new AiTranslateError(resp.error);
499
+ return { ok: false, error: resp.error };
500
500
  }
501
501
  let res;
502
502
  try {
@@ -511,7 +511,7 @@ export default class I18nPlugin extends AdminForthPlugin {
511
511
  failedReason: "Error in parsing LLM response"
512
512
  });
513
513
  });
514
- return null;
514
+ return { ok: false, error: 'Error in parsing LLM response' };
515
515
  }
516
516
  const backgroundJobsPlugin = this.adminforth.getPluginByClassName('BackgroundJobsPlugin');
517
517
  backgroundJobsPlugin.updateJobFieldsAtomically(jobId, () => __awaiter(this, void 0, void 0, function* () {
@@ -533,7 +533,7 @@ export default class I18nPlugin extends AdminForthPlugin {
533
533
  failedReason: "Error in parsing LLM response JSON"
534
534
  });
535
535
  });
536
- return null;
536
+ return { ok: false, error: 'Error in parsing LLM response JSON' };
537
537
  }
538
538
  for (const [schemaKey, translatedStr] of Object.entries(res)) {
539
539
  const enStr = schemaKeyToEnString.get(schemaKey) || schemaKey;
@@ -577,6 +577,7 @@ export default class I18nPlugin extends AdminForthPlugin {
577
577
  yield this.cache.clear(`${this.resourceConfig.resourceId}:${category}:${lang}`);
578
578
  }
579
579
  }
580
+ return { ok: true };
580
581
  });
581
582
  }
582
583
  getTranslateToLangTasks(langIsoCode_1, strings_1) {
@@ -793,8 +794,9 @@ export default class I18nPlugin extends AdminForthPlugin {
793
794
  //handler function
794
795
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ jobId, setTaskStateField, getTaskStateField }) {
795
796
  const initialState = yield getTaskStateField();
797
+ let bunchResult;
796
798
  if (initialState.prompt && initialState.strings && initialState.translations && initialState.updateStrings && initialState.lang && initialState.failedToTranslate && initialState.needToTranslateByLang) {
797
- yield this.generateAndSaveBunch(initialState.prompt, initialState.strings, initialState.translations, initialState.updateStrings, initialState.lang, initialState.failedToTranslate, initialState.needToTranslateByLang, jobId, initialState.promptCost);
799
+ bunchResult = yield this.generateAndSaveBunch(initialState.prompt, initialState.strings, initialState.translations, initialState.updateStrings, initialState.lang, initialState.failedToTranslate, initialState.needToTranslateByLang, jobId, initialState.promptCost);
798
800
  }
799
801
  afLogger.debug(`Translation task for language ${initialState.lang} completed.`);
800
802
  const stateToSave = {
@@ -804,6 +806,10 @@ export default class I18nPlugin extends AdminForthPlugin {
804
806
  };
805
807
  yield setTaskStateField(stateToSave);
806
808
  this.adminforth.websocket.publish('/translation_progress', {});
809
+ if (bunchResult && !bunchResult.ok) {
810
+ afLogger.error(`Failed to translate bunch for language ${initialState.lang} in plugin ${this.constructor.name}: ${bunchResult.error}`);
811
+ throw new AiTranslateError(bunchResult.error);
812
+ }
807
813
  if (initialState.failedToTranslate.length > 0) {
808
814
  afLogger.error(`Failed to translate some strings for language ${initialState.lang} in plugin ${this.constructor.name}:, ${initialState.failedToTranslate}`);
809
815
  throw new Error(`Failed to translate some strings for language ${initialState.lang}, check job details for more info`);
package/index.ts CHANGED
@@ -540,7 +540,7 @@ export default class I18nPlugin extends AdminForthPlugin {
540
540
  needToTranslateByLang: Record<string, any> = {},
541
541
  jobId: string,
542
542
  promptCost: number,
543
- ): Promise<void>{
543
+ ): Promise<{ ok: boolean, error?: string }>{
544
544
 
545
545
  // return [];
546
546
  const jsonSchemaProperties = {};
@@ -578,7 +578,7 @@ export default class I18nPlugin extends AdminForthPlugin {
578
578
  process.env.HEAVY_DEBUG && console.log(`🪲🔪LLM resp >> ${prompt.length}, <<${resp.content.length} :\n\n`, JSON.stringify(resp));
579
579
 
580
580
  if (resp.error) {
581
- throw new AiTranslateError(resp.error);
581
+ return { ok: false, error: resp.error };
582
582
  }
583
583
 
584
584
  let res;
@@ -593,7 +593,7 @@ export default class I18nPlugin extends AdminForthPlugin {
593
593
  failedReason: "Error in parsing LLM response"
594
594
  });
595
595
  });
596
- return null;
596
+ return { ok: false, error: 'Error in parsing LLM response' };
597
597
  }
598
598
 
599
599
  const backgroundJobsPlugin = this.adminforth.getPluginByClassName<any>('BackgroundJobsPlugin');
@@ -618,7 +618,7 @@ export default class I18nPlugin extends AdminForthPlugin {
618
618
  failedReason: "Error in parsing LLM response JSON"
619
619
  });
620
620
  });
621
- return null;
621
+ return { ok: false, error: 'Error in parsing LLM response JSON' };
622
622
  }
623
623
 
624
624
 
@@ -681,6 +681,7 @@ export default class I18nPlugin extends AdminForthPlugin {
681
681
  }
682
682
  }
683
683
 
684
+ return { ok: true };
684
685
  }
685
686
 
686
687
  async getTranslateToLangTasks (
@@ -976,9 +977,10 @@ export default class I18nPlugin extends AdminForthPlugin {
976
977
  needToTranslateByLang?: Record<string, any>,
977
978
  promptCost?: number,
978
979
  } = await getTaskStateField();
980
+ let bunchResult: { ok: boolean, error?: string } | undefined;
979
981
 
980
982
  if ( initialState.prompt && initialState.strings && initialState.translations && initialState.updateStrings && initialState.lang && initialState.failedToTranslate && initialState.needToTranslateByLang) {
981
- await this.generateAndSaveBunch(
983
+ bunchResult = await this.generateAndSaveBunch(
982
984
  initialState.prompt,
983
985
  initialState.strings,
984
986
  initialState.translations,
@@ -1000,6 +1002,10 @@ export default class I18nPlugin extends AdminForthPlugin {
1000
1002
  await setTaskStateField(stateToSave);
1001
1003
 
1002
1004
  this.adminforth.websocket.publish('/translation_progress', {});
1005
+ if (bunchResult && !bunchResult.ok) {
1006
+ afLogger.error(`Failed to translate bunch for language ${initialState.lang} in plugin ${this.constructor.name}: ${bunchResult.error}`);
1007
+ throw new AiTranslateError(bunchResult.error);
1008
+ }
1003
1009
  if (initialState.failedToTranslate.length > 0) {
1004
1010
  afLogger.error(`Failed to translate some strings for language ${initialState.lang} in plugin ${this.constructor.name}:, ${initialState.failedToTranslate}`);
1005
1011
  throw new Error(`Failed to translate some strings for language ${initialState.lang}, check job details for more info`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/i18n",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",