@adminforth/i18n 1.8.1 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.log +3 -2
- package/custom/BulkActionButton.vue +132 -0
- package/custom/langCommon.ts +2 -0
- package/custom/package-lock.json +9 -0
- package/custom/package.json +3 -0
- package/dist/custom/BulkActionButton.vue +132 -0
- package/dist/custom/langCommon.ts +2 -0
- package/dist/custom/package-lock.json +9 -0
- package/dist/custom/package.json +3 -0
- package/dist/index.js +77 -45
- package/index.ts +86 -47
- package/package.json +1 -1
- package/types.ts +7 -1
package/build.log
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
sending incremental file list
|
|
6
6
|
custom/
|
|
7
|
+
custom/BulkActionButton.vue
|
|
7
8
|
custom/LanguageEveryPageLoader.vue
|
|
8
9
|
custom/LanguageInUserMenu.vue
|
|
9
10
|
custom/LanguageUnderLogin.vue
|
|
@@ -17,5 +18,5 @@ custom/package-lock.json
|
|
|
17
18
|
custom/package.json
|
|
18
19
|
custom/tsconfig.json
|
|
19
20
|
|
|
20
|
-
sent
|
|
21
|
-
total size is
|
|
21
|
+
sent 38,848 bytes received 267 bytes 78,230.00 bytes/sec
|
|
22
|
+
total size is 37,840 speedup is 0.97
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Dialog
|
|
3
|
+
class="w-[500px]"
|
|
4
|
+
:buttons="[
|
|
5
|
+
{
|
|
6
|
+
label: 'Translate',
|
|
7
|
+
onclick: (dialog) => { runTranslation(); dialog.hide(); } ,
|
|
8
|
+
options: {
|
|
9
|
+
disabled: noneChecked
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
label: 'Close',
|
|
14
|
+
options: {
|
|
15
|
+
class: 'bg-white hover:!bg-gray-100 !text-gray-900 hover:!text-gray-800 dark:!bg-gray-800 dark:!text-gray-100 dark:hover:!bg-gray-700 !border-gray-200 dark:!border-gray-600'
|
|
16
|
+
},
|
|
17
|
+
onclick: (dialog) => dialog.hide()
|
|
18
|
+
},
|
|
19
|
+
]"
|
|
20
|
+
>
|
|
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
|
|
31
|
+
</div>
|
|
32
|
+
</button>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<div class="grid grid-cols-2 gap-4 w-full">
|
|
36
|
+
<Button @click="selectAll" :disabled="allChecked">{{ t('Select All') }}</Button>
|
|
37
|
+
<Button @click="uncheckAll" :disabled="noneChecked">{{ t('Uncheck All') }}</Button>
|
|
38
|
+
<div class="col-span-2 grid grid-cols-3 gap-4 ">
|
|
39
|
+
<div class="group flex items-center justify-between cursor-pointer" v-for="(index, lang) in checkedLanguages" :key="index" @click="toggleLanguage(lang)">
|
|
40
|
+
<div class="flex items-center gap-2">
|
|
41
|
+
<Checkbox v-model="checkedLanguages[lang]" />
|
|
42
|
+
<span class="flag-icon"
|
|
43
|
+
:class="`flag-icon-${getCountryCodeFromLangCode(lang)}`"
|
|
44
|
+
></span>
|
|
45
|
+
<span class="group-hover:underline">{{ getName(getCountryCodeFromLangCode(lang)) }}</span>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</Dialog>
|
|
51
|
+
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<script setup lang="ts">
|
|
55
|
+
import { IconLanguageOutline } from '@iconify-prerendered/vue-flowbite';
|
|
56
|
+
import { useI18n } from 'vue-i18n';
|
|
57
|
+
import { Dialog, Button, Checkbox } from '@/afcl';
|
|
58
|
+
import { computed, onMounted, ref, watch } from 'vue';
|
|
59
|
+
import { callAdminForthApi } from '@/utils';
|
|
60
|
+
import { useAdminforth } from '@/adminforth';
|
|
61
|
+
import { getCountryCodeFromLangCode } from './langCommon';
|
|
62
|
+
import { getName, overwrite } from 'country-list';
|
|
63
|
+
|
|
64
|
+
const { t } = useI18n();
|
|
65
|
+
const adminforth = useAdminforth();
|
|
66
|
+
|
|
67
|
+
overwrite([{
|
|
68
|
+
code: 'US',
|
|
69
|
+
name: 'USA'
|
|
70
|
+
}]);
|
|
71
|
+
const props = defineProps<{
|
|
72
|
+
resource: Record<string, any>;
|
|
73
|
+
checkboxes: string[];
|
|
74
|
+
adminUser: Record<string, any>;
|
|
75
|
+
meta: {
|
|
76
|
+
supportedLanguages: string[];
|
|
77
|
+
pluginInstanceId: string;
|
|
78
|
+
};
|
|
79
|
+
clearCheckboxes: () => void;
|
|
80
|
+
}>();
|
|
81
|
+
|
|
82
|
+
const checkedLanguages = ref<Record<string, boolean>>({});
|
|
83
|
+
const allChecked = computed(() => Object.values(checkedLanguages.value).every(Boolean));
|
|
84
|
+
const noneChecked = computed(() => Object.values(checkedLanguages.value).every(value => !value));
|
|
85
|
+
|
|
86
|
+
onMounted(() => {
|
|
87
|
+
for (const lang of props.meta.supportedLanguages) {
|
|
88
|
+
checkedLanguages.value[lang] = true;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
function selectAll() {
|
|
93
|
+
for (const lang of props.meta.supportedLanguages) {
|
|
94
|
+
checkedLanguages.value[lang] = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function uncheckAll() {
|
|
99
|
+
for (const lang of props.meta.supportedLanguages) {
|
|
100
|
+
checkedLanguages.value[lang] = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function toggleLanguage(lang: string) {
|
|
105
|
+
checkedLanguages.value[lang] = !checkedLanguages.value[lang];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function runTranslation() {
|
|
109
|
+
try {
|
|
110
|
+
const res = await callAdminForthApi({
|
|
111
|
+
path: `/plugin/${props.meta.pluginInstanceId}/translate-selected-to-languages`,
|
|
112
|
+
method: 'POST',
|
|
113
|
+
body: {
|
|
114
|
+
selectedIds: props.checkboxes,
|
|
115
|
+
selectedLanguages: Object.keys(checkedLanguages.value).filter(lang => checkedLanguages.value[lang]),
|
|
116
|
+
},
|
|
117
|
+
silentError: true,
|
|
118
|
+
});
|
|
119
|
+
adminforth.list.refresh();
|
|
120
|
+
props.clearCheckboxes();
|
|
121
|
+
if (res.ok) {
|
|
122
|
+
adminforth.alert({ message: res.successMessage, variant: 'success' });
|
|
123
|
+
} else {
|
|
124
|
+
adminforth.alert({ message: res.errorMessage || t('Failed to translate selected items. Please, try again.'), variant: 'danger' });
|
|
125
|
+
}
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.error('Failed to translate selected items:', e);
|
|
128
|
+
adminforth.alert({ message: t('Failed to translate selected items. Please, try again.'), variant: 'danger' });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
</script>
|
package/custom/langCommon.ts
CHANGED
|
@@ -76,6 +76,8 @@ const countryISO31661ByLangISO6391 = {
|
|
|
76
76
|
ja: 'jp', // Japanese → Japan
|
|
77
77
|
uk: 'ua', // Ukrainian → Ukraine
|
|
78
78
|
ur: 'pk', // Urdu → Pakistan
|
|
79
|
+
sr: 'rs', // Serbian → Serbia
|
|
80
|
+
da: 'dk' // Danish → Denmark
|
|
79
81
|
};
|
|
80
82
|
|
|
81
83
|
export function getCountryCodeFromLangCode(langCode) {
|
package/custom/package-lock.json
CHANGED
|
@@ -8,10 +8,19 @@
|
|
|
8
8
|
"name": "custom",
|
|
9
9
|
"version": "1.0.0",
|
|
10
10
|
"license": "ISC",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"country-list": "^2.4.1"
|
|
13
|
+
},
|
|
11
14
|
"devDependencies": {
|
|
12
15
|
"flag-icon-css": "^4.1.7"
|
|
13
16
|
}
|
|
14
17
|
},
|
|
18
|
+
"node_modules/country-list": {
|
|
19
|
+
"version": "2.4.1",
|
|
20
|
+
"resolved": "https://registry.npmjs.org/country-list/-/country-list-2.4.1.tgz",
|
|
21
|
+
"integrity": "sha512-KhVV/UfUV3dSNpsWIqHTQxLpYDKPKz1UwkRjadt+YbX2PRhyCEihEoS5XgB7J7AMXpkicvl+tRHvkNI5wbji/g==",
|
|
22
|
+
"license": "MIT"
|
|
23
|
+
},
|
|
15
24
|
"node_modules/flag-icon-css": {
|
|
16
25
|
"version": "4.1.7",
|
|
17
26
|
"resolved": "https://registry.npmjs.org/flag-icon-css/-/flag-icon-css-4.1.7.tgz",
|
package/custom/package.json
CHANGED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Dialog
|
|
3
|
+
class="w-[500px]"
|
|
4
|
+
:buttons="[
|
|
5
|
+
{
|
|
6
|
+
label: 'Translate',
|
|
7
|
+
onclick: (dialog) => { runTranslation(); dialog.hide(); } ,
|
|
8
|
+
options: {
|
|
9
|
+
disabled: noneChecked
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
label: 'Close',
|
|
14
|
+
options: {
|
|
15
|
+
class: 'bg-white hover:!bg-gray-100 !text-gray-900 hover:!text-gray-800 dark:!bg-gray-800 dark:!text-gray-100 dark:hover:!bg-gray-700 !border-gray-200 dark:!border-gray-600'
|
|
16
|
+
},
|
|
17
|
+
onclick: (dialog) => dialog.hide()
|
|
18
|
+
},
|
|
19
|
+
]"
|
|
20
|
+
>
|
|
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
|
|
31
|
+
</div>
|
|
32
|
+
</button>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<div class="grid grid-cols-2 gap-4 w-full">
|
|
36
|
+
<Button @click="selectAll" :disabled="allChecked">{{ t('Select All') }}</Button>
|
|
37
|
+
<Button @click="uncheckAll" :disabled="noneChecked">{{ t('Uncheck All') }}</Button>
|
|
38
|
+
<div class="col-span-2 grid grid-cols-3 gap-4 ">
|
|
39
|
+
<div class="group flex items-center justify-between cursor-pointer" v-for="(index, lang) in checkedLanguages" :key="index" @click="toggleLanguage(lang)">
|
|
40
|
+
<div class="flex items-center gap-2">
|
|
41
|
+
<Checkbox v-model="checkedLanguages[lang]" />
|
|
42
|
+
<span class="flag-icon"
|
|
43
|
+
:class="`flag-icon-${getCountryCodeFromLangCode(lang)}`"
|
|
44
|
+
></span>
|
|
45
|
+
<span class="group-hover:underline">{{ getName(getCountryCodeFromLangCode(lang)) }}</span>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</Dialog>
|
|
51
|
+
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<script setup lang="ts">
|
|
55
|
+
import { IconLanguageOutline } from '@iconify-prerendered/vue-flowbite';
|
|
56
|
+
import { useI18n } from 'vue-i18n';
|
|
57
|
+
import { Dialog, Button, Checkbox } from '@/afcl';
|
|
58
|
+
import { computed, onMounted, ref, watch } from 'vue';
|
|
59
|
+
import { callAdminForthApi } from '@/utils';
|
|
60
|
+
import { useAdminforth } from '@/adminforth';
|
|
61
|
+
import { getCountryCodeFromLangCode } from './langCommon';
|
|
62
|
+
import { getName, overwrite } from 'country-list';
|
|
63
|
+
|
|
64
|
+
const { t } = useI18n();
|
|
65
|
+
const adminforth = useAdminforth();
|
|
66
|
+
|
|
67
|
+
overwrite([{
|
|
68
|
+
code: 'US',
|
|
69
|
+
name: 'USA'
|
|
70
|
+
}]);
|
|
71
|
+
const props = defineProps<{
|
|
72
|
+
resource: Record<string, any>;
|
|
73
|
+
checkboxes: string[];
|
|
74
|
+
adminUser: Record<string, any>;
|
|
75
|
+
meta: {
|
|
76
|
+
supportedLanguages: string[];
|
|
77
|
+
pluginInstanceId: string;
|
|
78
|
+
};
|
|
79
|
+
clearCheckboxes: () => void;
|
|
80
|
+
}>();
|
|
81
|
+
|
|
82
|
+
const checkedLanguages = ref<Record<string, boolean>>({});
|
|
83
|
+
const allChecked = computed(() => Object.values(checkedLanguages.value).every(Boolean));
|
|
84
|
+
const noneChecked = computed(() => Object.values(checkedLanguages.value).every(value => !value));
|
|
85
|
+
|
|
86
|
+
onMounted(() => {
|
|
87
|
+
for (const lang of props.meta.supportedLanguages) {
|
|
88
|
+
checkedLanguages.value[lang] = true;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
function selectAll() {
|
|
93
|
+
for (const lang of props.meta.supportedLanguages) {
|
|
94
|
+
checkedLanguages.value[lang] = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function uncheckAll() {
|
|
99
|
+
for (const lang of props.meta.supportedLanguages) {
|
|
100
|
+
checkedLanguages.value[lang] = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function toggleLanguage(lang: string) {
|
|
105
|
+
checkedLanguages.value[lang] = !checkedLanguages.value[lang];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function runTranslation() {
|
|
109
|
+
try {
|
|
110
|
+
const res = await callAdminForthApi({
|
|
111
|
+
path: `/plugin/${props.meta.pluginInstanceId}/translate-selected-to-languages`,
|
|
112
|
+
method: 'POST',
|
|
113
|
+
body: {
|
|
114
|
+
selectedIds: props.checkboxes,
|
|
115
|
+
selectedLanguages: Object.keys(checkedLanguages.value).filter(lang => checkedLanguages.value[lang]),
|
|
116
|
+
},
|
|
117
|
+
silentError: true,
|
|
118
|
+
});
|
|
119
|
+
adminforth.list.refresh();
|
|
120
|
+
props.clearCheckboxes();
|
|
121
|
+
if (res.ok) {
|
|
122
|
+
adminforth.alert({ message: res.successMessage, variant: 'success' });
|
|
123
|
+
} else {
|
|
124
|
+
adminforth.alert({ message: res.errorMessage || t('Failed to translate selected items. Please, try again.'), variant: 'danger' });
|
|
125
|
+
}
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.error('Failed to translate selected items:', e);
|
|
128
|
+
adminforth.alert({ message: t('Failed to translate selected items. Please, try again.'), variant: 'danger' });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
</script>
|
|
@@ -76,6 +76,8 @@ const countryISO31661ByLangISO6391 = {
|
|
|
76
76
|
ja: 'jp', // Japanese → Japan
|
|
77
77
|
uk: 'ua', // Ukrainian → Ukraine
|
|
78
78
|
ur: 'pk', // Urdu → Pakistan
|
|
79
|
+
sr: 'rs', // Serbian → Serbia
|
|
80
|
+
da: 'dk' // Danish → Denmark
|
|
79
81
|
};
|
|
80
82
|
|
|
81
83
|
export function getCountryCodeFromLangCode(langCode) {
|
|
@@ -8,10 +8,19 @@
|
|
|
8
8
|
"name": "custom",
|
|
9
9
|
"version": "1.0.0",
|
|
10
10
|
"license": "ISC",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"country-list": "^2.4.1"
|
|
13
|
+
},
|
|
11
14
|
"devDependencies": {
|
|
12
15
|
"flag-icon-css": "^4.1.7"
|
|
13
16
|
}
|
|
14
17
|
},
|
|
18
|
+
"node_modules/country-list": {
|
|
19
|
+
"version": "2.4.1",
|
|
20
|
+
"resolved": "https://registry.npmjs.org/country-list/-/country-list-2.4.1.tgz",
|
|
21
|
+
"integrity": "sha512-KhVV/UfUV3dSNpsWIqHTQxLpYDKPKz1UwkRjadt+YbX2PRhyCEihEoS5XgB7J7AMXpkicvl+tRHvkNI5wbji/g==",
|
|
22
|
+
"license": "MIT"
|
|
23
|
+
},
|
|
15
24
|
"node_modules/flag-icon-css": {
|
|
16
25
|
"version": "4.1.7",
|
|
17
26
|
"resolved": "https://registry.npmjs.org/flag-icon-css/-/flag-icon-css-4.1.7.tgz",
|
package/dist/custom/package.json
CHANGED
package/dist/index.js
CHANGED
|
@@ -152,6 +152,13 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
152
152
|
throw new Error(`Invalid language code ${lang}. Use ISO 639-1 (e.g., 'en') or BCP-47 with region (e.g., 'en-GB').`);
|
|
153
153
|
}
|
|
154
154
|
});
|
|
155
|
+
if (this.options.translateLangAsBCP47Code) {
|
|
156
|
+
for (const [lang, bcp47] of Object.entries(this.options.translateLangAsBCP47Code)) {
|
|
157
|
+
if (!this.options.supportedLanguages.includes(lang)) {
|
|
158
|
+
throw new Error(`Invalid language code ${lang} in translateLangAsBCP47Code. It must be one of the supportedLanguages.`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
155
162
|
this.externalAppOnly = this.options.externalAppOnly === true;
|
|
156
163
|
// find primary key field
|
|
157
164
|
this.primaryKeyFieldName = (_a = resourceConfig.columns.find(c => c.primaryKey)) === null || _a === void 0 ? void 0 : _a.name;
|
|
@@ -388,45 +395,23 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
388
395
|
}));
|
|
389
396
|
}
|
|
390
397
|
// add bulk action
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
// if optional `confirm` is provided, user will be asked to confirm action
|
|
401
|
-
confirm: 'Are you sure you want to translate selected items? Only empty strings will be translated',
|
|
402
|
-
allowed: (_a) => __awaiter(this, [_a], void 0, function* ({ resource, adminUser, selectedIds, allowedActions }) {
|
|
403
|
-
process.env.HEAVY_DEBUG && console.log('allowedActions', JSON.stringify(allowedActions));
|
|
404
|
-
return allowedActions.edit;
|
|
405
|
-
}),
|
|
406
|
-
action: (_a) => __awaiter(this, [_a], void 0, function* ({ selectedIds, tr }) {
|
|
407
|
-
let translatedCount = 0;
|
|
408
|
-
try {
|
|
409
|
-
translatedCount = yield this.bulkTranslate({ selectedIds });
|
|
410
|
-
}
|
|
411
|
-
catch (e) {
|
|
412
|
-
process.env.HEAVY_DEBUG && console.error('🪲⛔ bulkTranslate error', e);
|
|
413
|
-
if (e instanceof AiTranslateError) {
|
|
414
|
-
return { ok: false, error: e.message };
|
|
415
|
-
}
|
|
416
|
-
throw e;
|
|
417
|
-
}
|
|
418
|
-
this.updateUntranslatedMenuBadge();
|
|
419
|
-
return {
|
|
420
|
-
ok: true,
|
|
421
|
-
error: undefined,
|
|
422
|
-
successMessage: yield tr(`Translated {count} items`, 'backend', {
|
|
423
|
-
count: translatedCount,
|
|
424
|
-
}),
|
|
425
|
-
};
|
|
426
|
-
})
|
|
427
|
-
});
|
|
398
|
+
const pageInjection = {
|
|
399
|
+
file: this.componentPath('BulkActionButton.vue'),
|
|
400
|
+
meta: {
|
|
401
|
+
supportedLanguages: this.options.supportedLanguages,
|
|
402
|
+
pluginInstanceId: this.pluginInstanceId,
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
if (!resourceConfig.options.pageInjections) {
|
|
406
|
+
resourceConfig.options.pageInjections = {};
|
|
428
407
|
}
|
|
429
|
-
|
|
408
|
+
if (!resourceConfig.options.pageInjections.list) {
|
|
409
|
+
resourceConfig.options.pageInjections.list = {};
|
|
410
|
+
}
|
|
411
|
+
if (!resourceConfig.options.pageInjections.list.beforeActionButtons) {
|
|
412
|
+
resourceConfig.options.pageInjections.list.beforeActionButtons = [];
|
|
413
|
+
}
|
|
414
|
+
resourceConfig.options.pageInjections.list.beforeActionButtons.push(pageInjection);
|
|
430
415
|
// if there is menu item with resourceId, add .badge function showing number of untranslated strings
|
|
431
416
|
const addBadgeCountToMenuItem = (menuItem) => {
|
|
432
417
|
this.menuItemWithBadgeId = menuItem.itemId;
|
|
@@ -458,6 +443,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
458
443
|
if (strings.length === 0) {
|
|
459
444
|
return [];
|
|
460
445
|
}
|
|
446
|
+
const replacedLanguageCodeForTranslations = this.options.translateLangAsBCP47Code && langIsoCode.length === 2 ? this.options.translateLangAsBCP47Code[langIsoCode] : null;
|
|
461
447
|
if (strings.length > maxKeysInOneReq) {
|
|
462
448
|
let totalTranslated = [];
|
|
463
449
|
for (let i = 0; i < strings.length; i += maxKeysInOneReq) {
|
|
@@ -468,14 +454,15 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
468
454
|
}
|
|
469
455
|
return totalTranslated;
|
|
470
456
|
}
|
|
457
|
+
const langCode = replacedLanguageCodeForTranslations ? replacedLanguageCodeForTranslations : langIsoCode;
|
|
471
458
|
const lang = langIsoCode;
|
|
472
459
|
const primaryLang = getPrimaryLanguageCode(lang);
|
|
473
460
|
const langName = iso6391.getName(primaryLang);
|
|
474
461
|
const requestSlavicPlurals = Object.keys(SLAVIC_PLURAL_EXAMPLES).includes(primaryLang) && plurals;
|
|
475
462
|
const region = ((_a = String(lang).split('-')[1]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || '';
|
|
476
463
|
const prompt = `
|
|
477
|
-
I need to translate strings in JSON to ${langName} language
|
|
478
|
-
${region ? `Use the regional conventions for ${
|
|
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.` : ''}
|
|
479
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]}"` : ''}
|
|
480
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:
|
|
481
468
|
|
|
@@ -486,8 +473,25 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
486
473
|
}, {}), null, 2)}
|
|
487
474
|
\`\`\`
|
|
488
475
|
`;
|
|
476
|
+
const jsonSchemaProperties = {};
|
|
477
|
+
strings.forEach(s => {
|
|
478
|
+
jsonSchemaProperties[s.en_string] = {
|
|
479
|
+
type: 'string',
|
|
480
|
+
minLength: 1,
|
|
481
|
+
};
|
|
482
|
+
});
|
|
483
|
+
const jsonSchemaRequired = strings.map(s => s.en_string);
|
|
489
484
|
// call OpenAI
|
|
490
|
-
const resp = yield this.options.completeAdapter.complete(prompt, [], prompt.length * 2
|
|
485
|
+
const resp = yield this.options.completeAdapter.complete(prompt, [], prompt.length * 2, {
|
|
486
|
+
json_schema: {
|
|
487
|
+
name: "translation_response",
|
|
488
|
+
schema: {
|
|
489
|
+
type: "object",
|
|
490
|
+
properties: jsonSchemaProperties,
|
|
491
|
+
required: jsonSchemaRequired,
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
});
|
|
491
495
|
process.env.HEAVY_DEBUG && console.log(`🪲🔪LLM resp >> ${prompt.length}, <<${resp.content.length} :\n\n`, JSON.stringify(resp));
|
|
492
496
|
if (resp.error) {
|
|
493
497
|
throw new AiTranslateError(resp.error);
|
|
@@ -499,7 +503,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
499
503
|
// ```
|
|
500
504
|
let res;
|
|
501
505
|
try {
|
|
502
|
-
res = resp.content
|
|
506
|
+
res = resp.content; //.split("```json")[1].split("```")[0];
|
|
503
507
|
}
|
|
504
508
|
catch (e) {
|
|
505
509
|
console.error(`Error in parsing LLM resp: ${resp}\n Prompt was: ${prompt}\n Resp was: ${JSON.stringify(resp)}`);
|
|
@@ -540,10 +544,11 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
540
544
|
}
|
|
541
545
|
// returns translated count
|
|
542
546
|
bulkTranslate(_a) {
|
|
543
|
-
return __awaiter(this, arguments, void 0, function* ({ selectedIds }) {
|
|
547
|
+
return __awaiter(this, arguments, void 0, function* ({ selectedIds, selectedLanguages }) {
|
|
544
548
|
const needToTranslateByLang = {};
|
|
545
549
|
const translations = yield this.adminforth.resource(this.resourceConfig.resourceId).list(Filters.IN(this.primaryKeyFieldName, selectedIds));
|
|
546
|
-
|
|
550
|
+
const languagesToProcess = selectedLanguages || this.options.supportedLanguages;
|
|
551
|
+
for (const lang of languagesToProcess) {
|
|
547
552
|
if (lang === 'en') {
|
|
548
553
|
// all strings are in English, no need to translate
|
|
549
554
|
continue;
|
|
@@ -799,7 +804,6 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
799
804
|
const flagEmoji = typeof getFlagEmoji === 'function'
|
|
800
805
|
? getFlagEmoji
|
|
801
806
|
: getFlagEmoji.default;
|
|
802
|
-
console.log('🪲languagesList for lang:', lang, 'country code:', getCountryCodeFromLangCode(lang));
|
|
803
807
|
return {
|
|
804
808
|
code: lang,
|
|
805
809
|
nameOnNative: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
|
|
@@ -897,5 +901,33 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
897
901
|
return { record: updatedRecord };
|
|
898
902
|
})
|
|
899
903
|
});
|
|
904
|
+
server.endpoint({
|
|
905
|
+
method: 'POST',
|
|
906
|
+
path: `/plugin/${this.pluginInstanceId}/translate-selected-to-languages`,
|
|
907
|
+
noAuth: false,
|
|
908
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, tr }) {
|
|
909
|
+
const selectedLanguages = body.selectedLanguages;
|
|
910
|
+
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();
|
|
923
|
+
return {
|
|
924
|
+
ok: true,
|
|
925
|
+
error: undefined,
|
|
926
|
+
successMessage: yield tr(`Translated {count} items`, 'backend', {
|
|
927
|
+
count: translatedCount,
|
|
928
|
+
}),
|
|
929
|
+
};
|
|
930
|
+
})
|
|
931
|
+
});
|
|
900
932
|
}
|
|
901
933
|
}
|
package/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import fs from 'fs-extra';
|
|
|
8
8
|
import chokidar from 'chokidar';
|
|
9
9
|
import { AsyncQueue } from '@sapphire/async-queue';
|
|
10
10
|
import getFlagEmoji from 'country-flag-svg';
|
|
11
|
-
import { parse } from 'bcp-47'
|
|
11
|
+
import { parse } from 'bcp-47';
|
|
12
12
|
|
|
13
13
|
const processFrontendMessagesQueue = new AsyncQueue();
|
|
14
14
|
|
|
@@ -172,6 +172,14 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
172
172
|
}
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
+
if (this.options.translateLangAsBCP47Code) {
|
|
176
|
+
for (const [lang, bcp47] of Object.entries(this.options.translateLangAsBCP47Code)) {
|
|
177
|
+
if (!this.options.supportedLanguages.includes(lang as SupportedLanguage)) {
|
|
178
|
+
throw new Error(`Invalid language code ${lang} in translateLangAsBCP47Code. It must be one of the supportedLanguages.`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
175
183
|
this.externalAppOnly = this.options.externalAppOnly === true;
|
|
176
184
|
|
|
177
185
|
// find primary key field
|
|
@@ -440,46 +448,27 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
440
448
|
}
|
|
441
449
|
|
|
442
450
|
// add bulk action
|
|
443
|
-
|
|
444
|
-
|
|
451
|
+
|
|
452
|
+
const pageInjection = {
|
|
453
|
+
file: this.componentPath('BulkActionButton.vue'),
|
|
454
|
+
meta: {
|
|
455
|
+
supportedLanguages: this.options.supportedLanguages,
|
|
456
|
+
pluginInstanceId: this.pluginInstanceId,
|
|
457
|
+
}
|
|
445
458
|
}
|
|
446
|
-
|
|
447
|
-
if (
|
|
448
|
-
resourceConfig.options.
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
},
|
|
460
|
-
action: async ({ selectedIds, tr }) => {
|
|
461
|
-
let translatedCount = 0;
|
|
462
|
-
try {
|
|
463
|
-
translatedCount = await this.bulkTranslate({ selectedIds });
|
|
464
|
-
} catch (e) {
|
|
465
|
-
process.env.HEAVY_DEBUG && console.error('🪲⛔ bulkTranslate error', e);
|
|
466
|
-
if (e instanceof AiTranslateError) {
|
|
467
|
-
return { ok: false, error: e.message };
|
|
468
|
-
}
|
|
469
|
-
throw e;
|
|
470
|
-
}
|
|
471
|
-
this.updateUntranslatedMenuBadge();
|
|
472
|
-
return {
|
|
473
|
-
ok: true,
|
|
474
|
-
error: undefined,
|
|
475
|
-
successMessage: await tr(`Translated {count} items`, 'backend', {
|
|
476
|
-
count: translatedCount,
|
|
477
|
-
}),
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
);
|
|
482
|
-
};
|
|
459
|
+
|
|
460
|
+
if (!resourceConfig.options.pageInjections) {
|
|
461
|
+
resourceConfig.options.pageInjections = {};
|
|
462
|
+
}
|
|
463
|
+
if (!resourceConfig.options.pageInjections.list) {
|
|
464
|
+
resourceConfig.options.pageInjections.list = {};
|
|
465
|
+
}
|
|
466
|
+
if (!resourceConfig.options.pageInjections.list.beforeActionButtons) {
|
|
467
|
+
resourceConfig.options.pageInjections.list.beforeActionButtons = [];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
(resourceConfig.options.pageInjections.list.beforeActionButtons as AdminForthComponentDeclaration[]).push(pageInjection);
|
|
471
|
+
|
|
483
472
|
|
|
484
473
|
// if there is menu item with resourceId, add .badge function showing number of untranslated strings
|
|
485
474
|
const addBadgeCountToMenuItem = (menuItem: AdminForthConfigMenuItem) => {
|
|
@@ -517,6 +506,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
517
506
|
return [];
|
|
518
507
|
}
|
|
519
508
|
|
|
509
|
+
const replacedLanguageCodeForTranslations = this.options.translateLangAsBCP47Code && langIsoCode.length === 2 ? this.options.translateLangAsBCP47Code[langIsoCode as any] : null;
|
|
520
510
|
if (strings.length > maxKeysInOneReq) {
|
|
521
511
|
let totalTranslated = [];
|
|
522
512
|
for (let i = 0; i < strings.length; i += maxKeysInOneReq) {
|
|
@@ -527,14 +517,15 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
527
517
|
}
|
|
528
518
|
return totalTranslated;
|
|
529
519
|
}
|
|
520
|
+
const langCode = replacedLanguageCodeForTranslations ? replacedLanguageCodeForTranslations : langIsoCode;
|
|
530
521
|
const lang = langIsoCode;
|
|
531
522
|
const primaryLang = getPrimaryLanguageCode(lang);
|
|
532
523
|
const langName = iso6391.getName(primaryLang);
|
|
533
524
|
const requestSlavicPlurals = Object.keys(SLAVIC_PLURAL_EXAMPLES).includes(primaryLang) && plurals;
|
|
534
525
|
const region = String(lang).split('-')[1]?.toUpperCase() || '';
|
|
535
526
|
const prompt = `
|
|
536
|
-
I need to translate strings in JSON to ${langName} language
|
|
537
|
-
${region ? `Use the regional conventions for ${
|
|
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.` : ''}
|
|
538
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]}"` : ''}
|
|
539
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:
|
|
540
531
|
|
|
@@ -548,11 +539,31 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
548
539
|
\`\`\`
|
|
549
540
|
`;
|
|
550
541
|
|
|
542
|
+
const jsonSchemaProperties = {};
|
|
543
|
+
strings.forEach(s => {
|
|
544
|
+
jsonSchemaProperties[s.en_string] = {
|
|
545
|
+
type: 'string',
|
|
546
|
+
minLength: 1,
|
|
547
|
+
};
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
const jsonSchemaRequired = strings.map(s => s.en_string);
|
|
551
|
+
|
|
551
552
|
// call OpenAI
|
|
552
553
|
const resp = await this.options.completeAdapter.complete(
|
|
553
554
|
prompt,
|
|
554
555
|
[],
|
|
555
556
|
prompt.length * 2,
|
|
557
|
+
{
|
|
558
|
+
json_schema: {
|
|
559
|
+
name: "translation_response",
|
|
560
|
+
schema: {
|
|
561
|
+
type: "object",
|
|
562
|
+
properties: jsonSchemaProperties,
|
|
563
|
+
required: jsonSchemaRequired,
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
}
|
|
556
567
|
);
|
|
557
568
|
|
|
558
569
|
process.env.HEAVY_DEBUG && console.log(`🪲🔪LLM resp >> ${prompt.length}, <<${resp.content.length} :\n\n`, JSON.stringify(resp));
|
|
@@ -568,7 +579,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
568
579
|
// ```
|
|
569
580
|
let res;
|
|
570
581
|
try {
|
|
571
|
-
res = resp.content
|
|
582
|
+
res = resp.content//.split("```json")[1].split("```")[0];
|
|
572
583
|
} catch (e) {
|
|
573
584
|
console.error(`Error in parsing LLM resp: ${resp}\n Prompt was: ${prompt}\n Resp was: ${JSON.stringify(resp)}`, );
|
|
574
585
|
return [];
|
|
@@ -613,7 +624,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
613
624
|
}
|
|
614
625
|
|
|
615
626
|
// returns translated count
|
|
616
|
-
async bulkTranslate({ selectedIds }: { selectedIds: string[] }): Promise<number> {
|
|
627
|
+
async bulkTranslate({ selectedIds, selectedLanguages }: { selectedIds: string[], selectedLanguages?: SupportedLanguage[] }): Promise<number> {
|
|
617
628
|
|
|
618
629
|
const needToTranslateByLang : Partial<
|
|
619
630
|
Record<
|
|
@@ -626,8 +637,8 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
626
637
|
> = {};
|
|
627
638
|
|
|
628
639
|
const translations = await this.adminforth.resource(this.resourceConfig.resourceId).list(Filters.IN(this.primaryKeyFieldName, selectedIds));
|
|
629
|
-
|
|
630
|
-
for (const lang of
|
|
640
|
+
const languagesToProcess = selectedLanguages || this.options.supportedLanguages;
|
|
641
|
+
for (const lang of languagesToProcess) {
|
|
631
642
|
if (lang === 'en') {
|
|
632
643
|
// all strings are in English, no need to translate
|
|
633
644
|
continue;
|
|
@@ -938,7 +949,6 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
938
949
|
? getFlagEmoji
|
|
939
950
|
: getFlagEmoji.default;
|
|
940
951
|
|
|
941
|
-
console.log('🪲languagesList for lang:', lang, 'country code:', getCountryCodeFromLangCode(lang));
|
|
942
952
|
|
|
943
953
|
return {
|
|
944
954
|
code: lang,
|
|
@@ -1057,6 +1067,35 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
1057
1067
|
}
|
|
1058
1068
|
});
|
|
1059
1069
|
|
|
1070
|
+
server.endpoint({
|
|
1071
|
+
method: 'POST',
|
|
1072
|
+
path: `/plugin/${this.pluginInstanceId}/translate-selected-to-languages`,
|
|
1073
|
+
noAuth: false,
|
|
1074
|
+
handler: async ({ body, tr }) => {
|
|
1075
|
+
const selectedLanguages = body.selectedLanguages;
|
|
1076
|
+
const selectedIds = body.selectedIds;
|
|
1077
|
+
|
|
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
|
+
return {
|
|
1090
|
+
ok: true,
|
|
1091
|
+
error: undefined,
|
|
1092
|
+
successMessage: await tr(`Translated {count} items`, 'backend', {
|
|
1093
|
+
count: translatedCount,
|
|
1094
|
+
}),
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1060
1099
|
}
|
|
1061
1100
|
|
|
1062
1101
|
}
|
package/package.json
CHANGED
package/types.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type SupportedLanguage = LanguageCode | Bcp47LanguageTag;
|
|
|
10
10
|
|
|
11
11
|
export interface PluginOptions {
|
|
12
12
|
|
|
13
|
-
/* List of ISO 639-1 language codes
|
|
13
|
+
/* List of language codes which you want to support. Can be either short ISO 639-1 language codes or/and BCP47 tags */
|
|
14
14
|
supportedLanguages: SupportedLanguage[];
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -68,4 +68,10 @@ export interface PluginOptions {
|
|
|
68
68
|
* Defaults to 'en' if not specified.
|
|
69
69
|
*/
|
|
70
70
|
primaryLanguage?: SupportedLanguage;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Ask translator to treat some code from supportedLanguages as exact BCP47 tag. Read docs for details.
|
|
74
|
+
* key - one of the values form supportedLanguages, value -BCP47 tag
|
|
75
|
+
*/
|
|
76
|
+
translateLangAsBCP47Code?: Partial<Record<LanguageCode, Bcp47LanguageTag>>;
|
|
71
77
|
}
|