@adminforth/i18n 1.10.1 → 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/build.log CHANGED
@@ -10,6 +10,7 @@ custom/LanguageInUserMenu.vue
10
10
  custom/LanguageUnderLogin.vue
11
11
  custom/ListCell.vue
12
12
  custom/SingleMultiInput.vue
13
+ custom/TranslationJobViewComponent.vue
13
14
  custom/datepickerLocales.ts
14
15
  custom/dayjsLocales.ts
15
16
  custom/eventBus.ts
@@ -18,5 +19,5 @@ custom/package-lock.json
18
19
  custom/package.json
19
20
  custom/tsconfig.json
20
21
 
21
- sent 39,396 bytes received 267 bytes 79,326.00 bytes/sec
22
- total size is 38,388 speedup is 0.97
22
+ sent 43,756 bytes received 286 bytes 88,084.00 bytes/sec
23
+ total size is 42,662 speedup is 0.97
@@ -4,7 +4,7 @@
4
4
  :buttons="[
5
5
  {
6
6
  label: 'Translate',
7
- onclick: (dialog) => { runTranslation(); dialog.hide(); } ,
7
+ onclick: async (dialog) => { await runTranslation(); dialog.hide(); } ,
8
8
  options: {
9
9
  disabled: noneChecked
10
10
  }
@@ -19,20 +19,21 @@
19
19
  ]"
20
20
  >
21
21
  <template #trigger>
22
- <button
23
- v-if="checkboxes.length > 0"
24
- class="flex gap-1 items-center py-1 px-3 text-sm font-medium text-lightListViewButtonText focus:outline-none bg-lightListViewButtonBackground rounded-default border border-lightListViewButtonBorder hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightListViewButtonFocusRing dark:focus:ring-darkListViewButtonFocusRing dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:border-darkListViewButtonBorder dark:hover:text-darkListViewButtonTextHover dark:hover:bg-darkListViewButtonBackgroundHover"
25
- >
26
- <IconLanguageOutline class="w-5 h-5" />
27
- {{ t('Translate Selected') }} {{ `(${checkboxes.length})` }}
28
- <div class="text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800
29
- font-medium rounded-sm text-xs px-1 ml-1 text-center ">
30
- AI
22
+ <button class="flex items-center justify-center w-full">
23
+ <IconLanguageOutline class="text-gray-500 dark:text-gray-400 w-5 h-5" />
24
+ <div class="flex items-end justify-start gap-2 cursor-pointer">
25
+ <p class="text-justify max-h-[18px] truncate max-w-[60vw] md:max-w-none">{{ t('Translate filtered') }}</p>
26
+ <div class="flex items-center justify-center text-white bg-gradient-to-r h-[18px] from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 font-medium rounded-md text-sm px-1 text-center">
27
+ {{t('AI')}}
28
+ </div>
31
29
  </div>
32
30
  </button>
33
31
  </template>
34
32
 
35
33
  <div class="af-i18n-translations-selector grid grid-cols-2 gap-1 w-full">
34
+ <div v-if="isLoading" class="top-0 left-0 z-10 absolute bg-black/30 w-full h-full rounded-lg flex items-center justify-center">
35
+ <Spinner class="w-10 h-10" />
36
+ </div>
36
37
  <Button @click="selectAll" :disabled="allChecked">{{ t('Select All') }}</Button>
37
38
  <Button @click="uncheckAll" :disabled="noneChecked">{{ t('Uncheck All') }}</Button>
38
39
  <div class="col-span-2 grid grid-cols-3 gap-1 mt-4">
@@ -52,14 +53,18 @@
52
53
  <script setup lang="ts">
53
54
  import { IconLanguageOutline } from '@iconify-prerendered/vue-flowbite';
54
55
  import { useI18n } from 'vue-i18n';
55
- import { Dialog, Button, Checkbox } from '@/afcl';
56
- import { computed, onMounted, ref, watch } from 'vue';
56
+ import { Dialog, Button, Checkbox, Spinner } from '@/afcl';
57
+ import { computed, onMounted, ref, onUnmounted } from 'vue';
57
58
  import { callAdminForthApi } from '@/utils';
58
59
  import { useAdminforth } from '@/adminforth';
59
60
  import { getCountryCodeFromLangCode } from './langCommon';
60
61
  import { getName, overwrite } from 'country-list';
61
62
  import ISO6391 from 'iso-639-1';
62
63
  import 'flag-icon-css/css/flag-icons.min.css';
64
+ import websocket from '@/websocket';
65
+ import { useFiltersStore } from '@/stores/filters';
66
+
67
+ const filtersStore = useFiltersStore();
63
68
 
64
69
  const { t } = useI18n();
65
70
  const adminforth = useAdminforth();
@@ -80,14 +85,22 @@
80
85
  }>();
81
86
 
82
87
  const checkedLanguages = ref<Record<string, boolean>>({});
88
+ const isLoading = ref(false);
83
89
  const allChecked = computed(() => Object.values(checkedLanguages.value).every(Boolean));
84
90
  const noneChecked = computed(() => Object.values(checkedLanguages.value).every(value => !value));
85
91
 
86
92
  onMounted(() => {
93
+ websocket.subscribe('/translation_progress', (data) => {
94
+ adminforth.list.refresh();
95
+ });
87
96
  for (const lang of props.meta.supportedLanguages) {
88
97
  checkedLanguages.value[lang] = true;
89
98
  }
90
99
  });
100
+
101
+ onUnmounted( () => {
102
+ websocket.unsubscribe('/translation_progress');
103
+ } )
91
104
 
92
105
  function selectAll() {
93
106
  for (const lang of props.meta.supportedLanguages) {
@@ -106,20 +119,28 @@
106
119
  }
107
120
 
108
121
  async function runTranslation() {
122
+ isLoading.value = true;
123
+ const listOfIds = await getListOfIds();
109
124
  try {
110
125
  const res = await callAdminForthApi({
111
126
  path: `/plugin/${props.meta.pluginInstanceId}/translate-selected-to-languages`,
112
127
  method: 'POST',
113
128
  body: {
114
- selectedIds: props.checkboxes,
129
+ selectedIds: listOfIds,
115
130
  selectedLanguages: Object.keys(checkedLanguages.value).filter(lang => checkedLanguages.value[lang]),
116
131
  },
117
132
  silentError: true,
118
133
  });
119
- adminforth.list.refresh();
120
134
  props.clearCheckboxes();
121
135
  if (res.ok) {
122
- adminforth.alert({ message: res.successMessage, variant: 'success' });
136
+ adminforth.alert({ message: `Running translation job`, variant: 'success' });
137
+ console.log('Received record IDs for filtered selector:', res);
138
+ const jobId = res.jobId;
139
+ if (jobId) {
140
+ console.log('Opening job info popup for jobId:', jobId);
141
+ //@ts-ignore
142
+ window.OpenJobInfoPopup(jobId);
143
+ }
123
144
  } else {
124
145
  adminforth.alert({ message: res.errorMessage || t('Failed to translate selected items. Please, try again.'), variant: 'danger' });
125
146
  }
@@ -127,6 +148,29 @@
127
148
  console.error('Failed to translate selected items:', e);
128
149
  adminforth.alert({ message: t('Failed to translate selected items. Please, try again.'), variant: 'danger' });
129
150
  }
151
+ isLoading.value = false;
152
+ }
153
+
154
+ async function getListOfIds() {
155
+ const filters = filtersStore.getFilters();
156
+ let res;
157
+ try {
158
+ res = await callAdminForthApi({
159
+ path: `/plugin/${props.meta.pluginInstanceId}/get_filtered_ids`,
160
+ method: 'POST',
161
+ body: { filters },
162
+ silentError: true,
163
+ });
164
+ } catch (e) {
165
+ console.error('Failed to get records for filtered selector:', e);
166
+ return [];
167
+ }
168
+ if (!res?.ok || !res?.recordIds) {
169
+ console.error('Failed to get records for filtered selector, response error:', res);
170
+ return [];
171
+ }
172
+ return res.recordIds;
130
173
  }
174
+
131
175
 
132
176
  </script>
@@ -0,0 +1,82 @@
1
+
2
+ <template>
3
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 my-3">
4
+ <div class="flex items-center space-x-1">
5
+ <span class=" text-gray-500">{{ t('Total tokens will be used for translation:') }}</span>
6
+ <span class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ new Number(props.job.state?.totalTranslationTokenCost).toLocaleString() || 0 }}</span>
7
+ </div>
8
+ <div class="flex items-center space-x-1">
9
+ <span class=" text-gray-500">{{ t('Total translation token used:') }}</span>
10
+ <span class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ new Number(props.job.state?.totalUsedTokens).toLocaleString() || 0 }}</span>
11
+ </div>
12
+ </div>
13
+ <div class="grid grid-cols-3 gap-2">
14
+ <div class="bg-gray-50 hover:bg-gray-100 transition-all px-2 py-2 rounded-md border max-w-64 w-full flex items-center gap-2" v-for="(task, index) in translationTasks" :key="index">
15
+ {{ task.state?.taskName }} to
16
+ <span class="flag-icon"
17
+ :class="`flag-icon-${getCountryCodeFromLangCode(task.state?.lang)}`"
18
+ ></span>
19
+ <component
20
+ :is="getCustomComponent({file: '@@/plugins/BackgroundJobsPlugin/StateToIcon.vue'})"
21
+ :status="task.status"
22
+ />
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+
28
+ <script setup lang="ts">
29
+ import { AdminForthComponentDeclarationFull } from 'adminforth';
30
+ import { useI18n } from 'vue-i18n'
31
+ import { onMounted, onUnmounted, ref } from 'vue';
32
+ import websocket from '@/websocket';
33
+ import { getCountryCodeFromLangCode } from './langCommon';
34
+ import { getCustomComponent } from '@/utils';
35
+
36
+ const { t } = useI18n();
37
+
38
+ const translationTasks = ref<{state: Record<string, any>, status: string}[]>([]);
39
+ const currentPaginationWindow = ref({limit: 25, offset: 0});
40
+
41
+ const props = defineProps<{
42
+ meta: any;
43
+ getJobTasks: (limit?: number, offset?: number) => Promise<
44
+ {tasks: {state: Record<string, any>, status: string}[], total: number}>;
45
+ job: {
46
+ id: string;
47
+ name: string;
48
+ status: 'IN_PROGRESS' | 'DONE' | 'DONE_WITH_ERRORS' | 'CANCELLED';
49
+ state: {
50
+ totalTranslationTokenCost: number;
51
+ totalUsedTokens: number;
52
+ };
53
+ progress: number; // 0 to 100
54
+ createdAt: Date;
55
+ customComponent?: AdminForthComponentDeclarationFull;
56
+ };
57
+ }>();
58
+
59
+ onMounted(async () => {
60
+ const {tasks, total} = await props.getJobTasks(currentPaginationWindow.value.limit, currentPaginationWindow.value.offset);
61
+ translationTasks.value = tasks;
62
+
63
+ websocket.subscribe(`/background-jobs-task-update/${props.job.id}`, (data: { taskIndex: number, status?: string, state?: Record<string, any> }) => {
64
+ if ( data.taskIndex <= currentPaginationWindow.value.offset + currentPaginationWindow.value.limit && data.taskIndex >= currentPaginationWindow.value.offset ) {
65
+ if (data.state) {
66
+ translationTasks.value[data.taskIndex].state = data.state;
67
+ }
68
+ if (data.status) {
69
+ translationTasks.value[data.taskIndex].status = data.status;
70
+ }
71
+ }
72
+ });
73
+
74
+
75
+ });
76
+
77
+ onUnmounted(() => {
78
+ websocket.unsubscribe(`/background-jobs-task-update/${props.job.id}`);
79
+ });
80
+
81
+
82
+ </script>
@@ -4,7 +4,7 @@
4
4
  :buttons="[
5
5
  {
6
6
  label: 'Translate',
7
- onclick: (dialog) => { runTranslation(); dialog.hide(); } ,
7
+ onclick: async (dialog) => { await runTranslation(); dialog.hide(); } ,
8
8
  options: {
9
9
  disabled: noneChecked
10
10
  }
@@ -19,20 +19,21 @@
19
19
  ]"
20
20
  >
21
21
  <template #trigger>
22
- <button
23
- v-if="checkboxes.length > 0"
24
- class="flex gap-1 items-center py-1 px-3 text-sm font-medium text-lightListViewButtonText focus:outline-none bg-lightListViewButtonBackground rounded-default border border-lightListViewButtonBorder hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightListViewButtonFocusRing dark:focus:ring-darkListViewButtonFocusRing dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:border-darkListViewButtonBorder dark:hover:text-darkListViewButtonTextHover dark:hover:bg-darkListViewButtonBackgroundHover"
25
- >
26
- <IconLanguageOutline class="w-5 h-5" />
27
- {{ t('Translate Selected') }} {{ `(${checkboxes.length})` }}
28
- <div class="text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800
29
- font-medium rounded-sm text-xs px-1 ml-1 text-center ">
30
- AI
22
+ <button class="flex items-center justify-center w-full">
23
+ <IconLanguageOutline class="text-gray-500 dark:text-gray-400 w-5 h-5" />
24
+ <div class="flex items-end justify-start gap-2 cursor-pointer">
25
+ <p class="text-justify max-h-[18px] truncate max-w-[60vw] md:max-w-none">{{ t('Translate filtered') }}</p>
26
+ <div class="flex items-center justify-center text-white bg-gradient-to-r h-[18px] from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 font-medium rounded-md text-sm px-1 text-center">
27
+ {{t('AI')}}
28
+ </div>
31
29
  </div>
32
30
  </button>
33
31
  </template>
34
32
 
35
33
  <div class="af-i18n-translations-selector grid grid-cols-2 gap-1 w-full">
34
+ <div v-if="isLoading" class="top-0 left-0 z-10 absolute bg-black/30 w-full h-full rounded-lg flex items-center justify-center">
35
+ <Spinner class="w-10 h-10" />
36
+ </div>
36
37
  <Button @click="selectAll" :disabled="allChecked">{{ t('Select All') }}</Button>
37
38
  <Button @click="uncheckAll" :disabled="noneChecked">{{ t('Uncheck All') }}</Button>
38
39
  <div class="col-span-2 grid grid-cols-3 gap-1 mt-4">
@@ -52,14 +53,18 @@
52
53
  <script setup lang="ts">
53
54
  import { IconLanguageOutline } from '@iconify-prerendered/vue-flowbite';
54
55
  import { useI18n } from 'vue-i18n';
55
- import { Dialog, Button, Checkbox } from '@/afcl';
56
- import { computed, onMounted, ref, watch } from 'vue';
56
+ import { Dialog, Button, Checkbox, Spinner } from '@/afcl';
57
+ import { computed, onMounted, ref, onUnmounted } from 'vue';
57
58
  import { callAdminForthApi } from '@/utils';
58
59
  import { useAdminforth } from '@/adminforth';
59
60
  import { getCountryCodeFromLangCode } from './langCommon';
60
61
  import { getName, overwrite } from 'country-list';
61
62
  import ISO6391 from 'iso-639-1';
62
63
  import 'flag-icon-css/css/flag-icons.min.css';
64
+ import websocket from '@/websocket';
65
+ import { useFiltersStore } from '@/stores/filters';
66
+
67
+ const filtersStore = useFiltersStore();
63
68
 
64
69
  const { t } = useI18n();
65
70
  const adminforth = useAdminforth();
@@ -80,14 +85,22 @@
80
85
  }>();
81
86
 
82
87
  const checkedLanguages = ref<Record<string, boolean>>({});
88
+ const isLoading = ref(false);
83
89
  const allChecked = computed(() => Object.values(checkedLanguages.value).every(Boolean));
84
90
  const noneChecked = computed(() => Object.values(checkedLanguages.value).every(value => !value));
85
91
 
86
92
  onMounted(() => {
93
+ websocket.subscribe('/translation_progress', (data) => {
94
+ adminforth.list.refresh();
95
+ });
87
96
  for (const lang of props.meta.supportedLanguages) {
88
97
  checkedLanguages.value[lang] = true;
89
98
  }
90
99
  });
100
+
101
+ onUnmounted( () => {
102
+ websocket.unsubscribe('/translation_progress');
103
+ } )
91
104
 
92
105
  function selectAll() {
93
106
  for (const lang of props.meta.supportedLanguages) {
@@ -106,20 +119,28 @@
106
119
  }
107
120
 
108
121
  async function runTranslation() {
122
+ isLoading.value = true;
123
+ const listOfIds = await getListOfIds();
109
124
  try {
110
125
  const res = await callAdminForthApi({
111
126
  path: `/plugin/${props.meta.pluginInstanceId}/translate-selected-to-languages`,
112
127
  method: 'POST',
113
128
  body: {
114
- selectedIds: props.checkboxes,
129
+ selectedIds: listOfIds,
115
130
  selectedLanguages: Object.keys(checkedLanguages.value).filter(lang => checkedLanguages.value[lang]),
116
131
  },
117
132
  silentError: true,
118
133
  });
119
- adminforth.list.refresh();
120
134
  props.clearCheckboxes();
121
135
  if (res.ok) {
122
- adminforth.alert({ message: res.successMessage, variant: 'success' });
136
+ adminforth.alert({ message: `Running translation job`, variant: 'success' });
137
+ console.log('Received record IDs for filtered selector:', res);
138
+ const jobId = res.jobId;
139
+ if (jobId) {
140
+ console.log('Opening job info popup for jobId:', jobId);
141
+ //@ts-ignore
142
+ window.OpenJobInfoPopup(jobId);
143
+ }
123
144
  } else {
124
145
  adminforth.alert({ message: res.errorMessage || t('Failed to translate selected items. Please, try again.'), variant: 'danger' });
125
146
  }
@@ -127,6 +148,29 @@
127
148
  console.error('Failed to translate selected items:', e);
128
149
  adminforth.alert({ message: t('Failed to translate selected items. Please, try again.'), variant: 'danger' });
129
150
  }
151
+ isLoading.value = false;
152
+ }
153
+
154
+ async function getListOfIds() {
155
+ const filters = filtersStore.getFilters();
156
+ let res;
157
+ try {
158
+ res = await callAdminForthApi({
159
+ path: `/plugin/${props.meta.pluginInstanceId}/get_filtered_ids`,
160
+ method: 'POST',
161
+ body: { filters },
162
+ silentError: true,
163
+ });
164
+ } catch (e) {
165
+ console.error('Failed to get records for filtered selector:', e);
166
+ return [];
167
+ }
168
+ if (!res?.ok || !res?.recordIds) {
169
+ console.error('Failed to get records for filtered selector, response error:', res);
170
+ return [];
171
+ }
172
+ return res.recordIds;
130
173
  }
174
+
131
175
 
132
176
  </script>
@@ -0,0 +1,82 @@
1
+
2
+ <template>
3
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 my-3">
4
+ <div class="flex items-center space-x-1">
5
+ <span class=" text-gray-500">{{ t('Total tokens will be used for translation:') }}</span>
6
+ <span class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ new Number(props.job.state?.totalTranslationTokenCost).toLocaleString() || 0 }}</span>
7
+ </div>
8
+ <div class="flex items-center space-x-1">
9
+ <span class=" text-gray-500">{{ t('Total translation token used:') }}</span>
10
+ <span class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ new Number(props.job.state?.totalUsedTokens).toLocaleString() || 0 }}</span>
11
+ </div>
12
+ </div>
13
+ <div class="grid grid-cols-3 gap-2">
14
+ <div class="bg-gray-50 hover:bg-gray-100 transition-all px-2 py-2 rounded-md border max-w-64 w-full flex items-center gap-2" v-for="(task, index) in translationTasks" :key="index">
15
+ {{ task.state?.taskName }} to
16
+ <span class="flag-icon"
17
+ :class="`flag-icon-${getCountryCodeFromLangCode(task.state?.lang)}`"
18
+ ></span>
19
+ <component
20
+ :is="getCustomComponent({file: '@@/plugins/BackgroundJobsPlugin/StateToIcon.vue'})"
21
+ :status="task.status"
22
+ />
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+
28
+ <script setup lang="ts">
29
+ import { AdminForthComponentDeclarationFull } from 'adminforth';
30
+ import { useI18n } from 'vue-i18n'
31
+ import { onMounted, onUnmounted, ref } from 'vue';
32
+ import websocket from '@/websocket';
33
+ import { getCountryCodeFromLangCode } from './langCommon';
34
+ import { getCustomComponent } from '@/utils';
35
+
36
+ const { t } = useI18n();
37
+
38
+ const translationTasks = ref<{state: Record<string, any>, status: string}[]>([]);
39
+ const currentPaginationWindow = ref({limit: 25, offset: 0});
40
+
41
+ const props = defineProps<{
42
+ meta: any;
43
+ getJobTasks: (limit?: number, offset?: number) => Promise<
44
+ {tasks: {state: Record<string, any>, status: string}[], total: number}>;
45
+ job: {
46
+ id: string;
47
+ name: string;
48
+ status: 'IN_PROGRESS' | 'DONE' | 'DONE_WITH_ERRORS' | 'CANCELLED';
49
+ state: {
50
+ totalTranslationTokenCost: number;
51
+ totalUsedTokens: number;
52
+ };
53
+ progress: number; // 0 to 100
54
+ createdAt: Date;
55
+ customComponent?: AdminForthComponentDeclarationFull;
56
+ };
57
+ }>();
58
+
59
+ onMounted(async () => {
60
+ const {tasks, total} = await props.getJobTasks(currentPaginationWindow.value.limit, currentPaginationWindow.value.offset);
61
+ translationTasks.value = tasks;
62
+
63
+ websocket.subscribe(`/background-jobs-task-update/${props.job.id}`, (data: { taskIndex: number, status?: string, state?: Record<string, any> }) => {
64
+ if ( data.taskIndex <= currentPaginationWindow.value.offset + currentPaginationWindow.value.limit && data.taskIndex >= currentPaginationWindow.value.offset ) {
65
+ if (data.state) {
66
+ translationTasks.value[data.taskIndex].state = data.state;
67
+ }
68
+ if (data.status) {
69
+ translationTasks.value[data.taskIndex].status = data.status;
70
+ }
71
+ }
72
+ });
73
+
74
+
75
+ });
76
+
77
+ onUnmounted(() => {
78
+ websocket.unsubscribe(`/background-jobs-task-update/${props.job.id}`);
79
+ });
80
+
81
+
82
+ </script>